Mockito无法mock/spy情况下的解决方法

Mockito不支持mock/spy如下的类型:

  • final classes
  • anonymous classes
  • primitive types

如果你mock这些类型,会抛出如下错误信息:
org.mockito.exceptions.base.MockitoException: Cannot mock/spy class java.lang.String Mockito cannot mock/spy following: - final classes - anonymous classes - primitive types

而如果你有确实有此需求,可以通过如下方法解决。先定义一个工具类,里面包含如下方法:

public static void injectField(final Object injectable, final String fieldname, final Object value) {
  try {
    final java.lang.reflect.Field field = injectable.getClass().getDeclaredField(fieldname);
    final boolean origionalValue = field.isAccessible();
    field.setAccessible(true);
    field.set(injectable, value);
    field.setAccessible(origionalValue);
  } catch (final NoSuchFieldException | IllegalAccessException e) {
    throw new RuntimeException(e.getMessage(), e);
  }
}

比如你有如下类需要测试:

public class Greeting {
  @Inject
  private String greeting;

  @Override
  public String toString() {
    return this.greeting;
  }
}

那么,你的测试类大概如下:

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;

public class GreetingTest {
  @Test
  public void testToString() throws Exception {
    Greeting greeting = new Greeting();
    injectField(greeting, "greeting", "Hello, world");
    assertThat(greeting.toString(), is("Hello, world"));
  }
}

以上参考:https://dzone.com/articles/field-injection-when-mocking-frameworks-fail

当然,如果你使用Spring Test,你可以使用org.springframework.test.util.ReflectionTestUtils。 类似ReflectionTestUtils.setField(testObject, "person", mockedPerson);

或者,如果你使用Mockito 1.*,可以使用
import org.mockito.internal.util.reflection.FieldSetter;
new FieldSetter(obj, obj.getClass().getDeclaredField("p")).set(new P());

//or
org.mockito.internal.util.reflection.Whitebox#setInternalState(Object target, String field, Object value)

针对Mockito 2.*,可以使用:
org.mockito.internal.util.reflection.FieldSetter.setField(Object target, Field field, Object value)

使用Mockito来mock autowired字段

依赖注入(DI)是像Spring,EJB这样的控制反转(IOC)类容器的一个很强大的特性。把注入的值封装成类的私有字段是一个很好的主意,但是像这样把autowired的字段封装起来也降低了可测试性。本文将介绍针对这种情况,Mockito是如何来mock私有的autowired字段的。

首先被测试的类依赖的第一个类是这样子的,它是一个Spring单例bean,这个类将会在测试中被mock掉。


@Repository
public class OrderDao {
  public Order getOrder(int irderId){
    throw new UnsupportedOperationException("Fail is not mocked!");
  }
}

接下来是被测试的类依赖的第二个类,它也是Spring的一个Component。在测试中,这个类将会被spied(partially mocked)。它的方法calculatePriceForOrder将会原样被调用(不被stub),另外一个方法将会被stubbed。

@Service
public class PriceService {

  public int getActualPrice(Item item){
    throw new UnsupportedOperationException("Fail is not mocked!");
  }

  public int calculatePriceForOrder(Order order){
    int orderPrice = 0;
    for (Item item : order.getItems()){
      orderPrice += getActualPrice(item);
    }
    return orderPrice;
  }

}

然后是被测试的类,它使用autowire上面的两个依赖的类。

@Service
public class OrderService {

  @Autowired
  private PriceService priceService;

  @Autowired
  private OrderDao orderDao;

  public int getOrderPrice(int orderId){
    Order order = orderDao.getOrder(orderId);
    return priceService.calculatePriceForOrder(order);
  }

}

最后是测试类,它使用字段级别的注解:

  • @InjectMocks – 含有此注解的字段,Mockito将会实例化被测试的类,并且尝试把用@Mock或者@Spy注解的字段注入到被测试对象的私有字段中。
  • @Mock – 含有此注解的字段,Mockito会创建对应类的mock实例。
  • @Spy – 含有此注解的字段,Mockito会创建对应类的spy实例。
  • public class OrderServiceTest {
    
      private static final int TEST_ORDER_ID = 15;
      private static final int TEST_SHOES_PRICE = 2;
      private static final int TEST_SHIRT_PRICE = 1;
    
      @InjectMocks
      private OrderService testingObject;
      @Spy
      private PriceService priceService;
      @Mock
      private OrderDao orderDao;
    
      @Before
      public void initMocks(){
        MockitoAnnotations.initMocks(this);
      }
    
      @Test
      public void testGetOrderService(){
        Order order = new Order(Arrays.asList(Item.SHOES, Item.SHIRT));
        Mockito.when(orderDao.getOrder(TEST_ORDER_ID)).thenReturn(order);
        //notice different Mockito syntax for spy
        Mockito.doReturn(TEST_SHIRT_PRICE).when(priceService).getActualPrice(Item.SHIRT);
        Mockito.doReturn(TEST_SHOES_PRICE).when(priceService).getActualPrice(Item.SHOES);
        //call testing method
        int actualOrderPrice = testingObject.getOrderPrice(TEST_ORDER_ID);
        Assert.assertEquals(TEST_SHIRT_PRICE + TEST_SHOES_PRICE, actualOrderPrice);
      }
    
    }
    

    让我们看下在执行这个测试类的时候会发生什么:
    1)首先JUnit框架会在执行每个测试方法之前执行@Before注解的方法initMocks。
    2)initMocks这个方法会调用Mockito的方法(MockitoAnnotations.initMocks(this))初始化被被注解的字段。如果没有这个调用,那么被注解的字段将会是null。也可以使用@RunWith(MockitoJUnitRunner.class)来注解被测试的类来达成相同的效果。
    3)最后当所有的字段被初始化完成后,开始执行测试方法(被@Test注解的方法)。

    这个例子中不包含Spring context的创建,这里的Spring注解(比如@Service,@Repository)只是为了说明实际生产中代码的例子。测试本身不包含对Spring的任何依赖并且或忽略Spring的任何注解。事实上这里可以换成EJB的注解,或者甚至换成普通的私有字段(不被任何IoC容器管理),测试依然可以执行。

    开发者可能认为每次测试之前的 MockitoAnnotations.initMocks(this)调用是不必要的开销。但是其实这样很方便,因为它重置了被测试的对象和重新初始化了mock对象。当你的测试类里有多个测试方法(@Test注解的方法)时,可以避免互相干扰。

    @Spy注解的对象可以通过两种方法来初始化:
    1)如果有默认的构造器,则可以由Mockito框架来自动创建;
    2)如果仅有非默认构造器,可以显示实例化它。比如
    @Spy
    private PriceService priceService = new PriceService();

    类似的,被@InjectMocks注解的被测试对象也可以显式实例化。

    参考:https://lkrnac.net/blog/2014/01/mock-autowired-fields/

    Java如何校验IP/Domain格式

    说到校验IP地址或者Domain的格式是否正确,很多人可能是想到用正则表达式来校验。
    一种比较好的做法是使用现成的库来完成此功能,尤其是比较流行的类库,经过了大量的验证,比自己实现要安全可靠。
    本文推荐使用Google的Guava库里的工具类来校验。

    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>19.0</version>
    </dependency>
    

    校验IP地址格式的有效性

    //com.google.common.net.InetAddresses#isInetAddress(String ipString)
    
    InetAddresses.isInetAddress("1.1.1.1");//true
    InetAddresses.isInetAddress("1.1.1");//false
    InetAddresses.isInetAddress("127.0.0.1");//true
    InetAddresses.isInetAddress("fc00::");//true
    InetAddresses.isInetAddress("localhost");//false
    InetAddresses.isInetAddress("256.256.1212.1");//false
    InetAddresses.isInetAddress("256.256.257.1");//false
    InetAddresses.isInetAddress("300.300.300.300");//false
    
    //校验ip是ipv4地址还是ipv6地址
    InetAddresses.forString("fc00::") instanceof Inet6Address;//true
    InetAddresses.forString("127.0.0.1") instanceof Inet4Address;//true
    

    校验Domain的合法性

    //com.google.common.net.InternetDomainName#isValid(String name)
    
    InternetDomainName.isValid("www.baidu.com"); //true
    InternetDomainName.isValid("127.0.0.1"); //false
    InternetDomainName.isValid("a.b.c.com"); //true
    InternetDomainName.isValid("http://a.b.c.com"); //false
    InternetDomainName.isValid("http://a.b.c.com/"); //false
    

    Url的host部分合法性的校验

    //com.google.common.net.HostSpecifier#isValid(String specifier)
    
    //检查是否可以作为url的host部分,可能是domain或者ip
    HostSpecifier.isValid("a.b.c.com");//true
    HostSpecifier.isValid("a.b.c.com:90");//false
    HostSpecifier.isValid("http://a.b.c.com");//false
    HostSpecifier.isValid("127.0.0.1");//true
    HostSpecifier.isValid("127.0.0.1:80");//false
    HostSpecifier.isValid("111::");//true
    

    javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

    最近在使用Java的HttpsURLConnection来访问https网站时,抛出了一个异常:

    javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

    代码大致如下:

    URL url = new URL("https://www.google.com/");
    HttpsURLConnection connection = (HttpsURLConnection)url.openConnection();
    connection.setHostnameVerifier((t, v) -> true);
    BufferedInputStream inputStream = new BufferedInputStream(connection.getInputStream());
    int respInt = inputStream.read();
    while (respInt != -1) {
    respInt = inputStream.read();
    }
    inputStream.close();

    网上查了下,发现很多情况都可能导致抛出这个异常,一种可行的排查方法是,在执行Java程序时,加
    -Djavax.net.debug=all 选项打开SSL连接时的debug开关,这样就会把建立连接的信息打印到控制台。

    这样执行程序时可以在控制台打印类似的消息(部分摘取):

    keyStore is :
    keyStore type is : jks
    keyStore provider is :
    init keystore
    init keymanager of type SunX509
    trustStore is: /Library/Java/JavaVirtualMachines/jdk1.8.0_66.jdk/Contents/Home/jre/lib/security/cacerts
    trustStore type is : jks
    trustStore provider is :
    init truststore
    adding as trusted cert:
    ......
    *** ClientHello, TLSv1.2
    RandomCookie: GMT: 1527255937 bytes = { 72, 31, 116, 116, 76, 130, 134, 23, 59, 194, 6, 185, 160, 110, 14, 131, 74, 175, 192, 56, 83, 130, 176, 102, 53, 50, 126, 139 }
    Session ID: {}
    Cipher Suites: [TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_128_CBC_SHA256, TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256, TLS_DHE_RSA_WITH_AES_128_CBC_SHA256, ...]
    Compression Methods: { 0 }
    Extension elliptic_curves, curve names: {secp256r1, sect163k1, sect163r2, secp192r1, secp224r1, sect233k1, sect233r1, sect283k1, sect283r1, secp384r1, sect409k1, ...}
    Extension ec_point_formats, formats: [uncompressed]
    Extension signature_algorithms, signature_algorithms: SHA512withECDSA, SHA512withRSA, SHA384withECDSA, SHA384withRSA, SHA256withECDSA, SHA256withRSA, SHA224withECDSA, SHA224withRSA, SHA1withECDSA, SHA1withRSA, SHA1withDSA, MD5withRSA
    ***
    ......
    [Raw read]: length = 5
    0000: 15 03 03 00 02 .....
    [Raw read]: length = 2
    0000: 02 28 .(
    main, READ: TLSv1.2 Alert, length = 2
    main, RECV TLSv1.2 ALERT: fatal, handshake_failure
    main, called closeSocket()
    main, handling exception: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

    也就是在clientHello阶段,服务器端返回了握手失败。而这一步握手失败有很多原因:

    • 客户端和服务器端的cipher suites不兼容
    • 客户端和服务器端使用的ssl协议版本不一致。(sslv2, sslv3, tlsv1.0, tlsv1.1, tlsv1.2)
    • 服务器的根证书不在客户端的可信目录中

    然而,在尝试了以上各种原因后,依然不行,正要崩溃时,在网上看到了关于JDK bug导致此错误的案例。

    简单来说就是,JDK1.8的特定版本,如果自定义HttpsURLConnection的HostnameVerifier(类似上面的代码中的connection.setHostnameVerifier((t, v) -> true);)会导致握手的ClientHello阶段,client不发送SNI extension。(注意,可能你的代码中没有自定义,但是可能依赖的jar包中有自定义的操作)

    正常情况下在ClientHello阶段,会发送:

    Extension server_name, server_name: [type=host_name (0), value=www.google.com]

    而在上面的出错的信息中,没有这个extension。

    解决办法,升级到JDK1.8的8u152版本或者以上,或者降低到u66版本以下。

    另外,如果不升级JDK,可以参见参考文档2来暂时规避掉此BUG.

    另外,不是所有的网站都会出现这个问题,理论上来说与SNI相关的网站出问题的可能比较大。关于SNI(Server Name Indication),可以参考下:https://en.wikipedia.org/wiki/Server_Name_Indication

    参考文档:

    1)介绍了这类问题的参考思路:

    https://stackoverflow.com/questions/6353849/received-fatal-alert-handshake-failure-through-sslhandshakeexception?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa

    2)分析并解决了我这种情况的问题:

    https://stackoverflow.com/questions/41692736/all-trusting-hostnameverifier-causes-ssl-errors-with-httpurlconnection

    3)JDK bug详情:

    https://bugs.java.com/view_bug.do?bug_id=JDK-8144566

     

    一个好用的IntelliJ IDEA的Maven插件

    Maven工程,经常需要查找冲突的jar包,一个常用的方式是在命令行执行:

    mvn dependency:tree 

    然后在打印出来的依赖树中找到依赖的(包括间接依赖)的jar包的不同版本。
    如果依赖较多的话,可以将其输出到一个文件中,然后便于查找,像这样:

    mvn dependency:tree > dependency.txt

    如果使用Intellij IDEA的话,可以安装Maven Helper插件。

    具体可以在IDEA中依次点击:File-Settings-Plugins-browser repositories, 然后在搜索框输入 Maven Helper搜索,即可搜出来进行安装。安装后需要重启下。

    安装完成后,就可以在IDEA中打开项目的pom.xml文件,在编辑器面板的左下角,可以发现多了一个TAB,Dependency Analyzer,点击切换到此面板,就可以在此查看冲突的jar包,也可以查找jar,通过点击查看依赖树,右击包名可以跳转到pom.xml文件中的对应位置,甚至可以直接exclude此二方包。更多功能可以自己安装后探索下, 或者参考附录的链接了解此插件的更多信息。

    参考:
    https://plugins.jetbrains.com/plugin/7179-maven-helper

    Maven报AetherClassNotFound异常

    maven 3.3 执行mvn dependency:tree时,会报出如下异常:

    Error injecting: org.apache.maven.shared.dependency.graph.internal.Maven3DependencyGraphBuilder java.lang.NoClassDefFoundError: org/sonatype/aether/graph/Dependency

    原因是因为,在Maven 3.1-alpha-1 从Sonatype Aether迁移到了Eclipse Aether,而这个变更对于某些插件(比如maven-dependency-plugin, android-maven-plugin, maven-shade-plugin, maven-site-plugin)是不兼容的。所以会报出找不到相关的类。

    解决方法是:使用Maven 3.1.*之前的版本,比如3.0.5。或者根据如下链接,升级对应的插件的版本:https://cwiki.apache.org/confluence/display/MAVEN/AetherClassNotFound

    Java NIO – Buffer

    Buffer是一块将要写到Channel里的数据,或者刚从Channel里读出来的数据。它是用来保存数据的一个对象,并且作为NIO Channel的其中一个端点(另一端可能是网络或者文件等)。Buffer提供了一种访问数据的机制,并且跟踪数据读写流程。

    Buffer是Java 老的IO和NIO的主要不同之一。以前老的IO直接从流中读或者写。现在数据是从Buffer中都数据或者将写的数据写入Buffer中。在NIO中,Channel就相当于以前的Stream。了解更多NIO Channel可以访问这里。

    Buffer特点:
    1 Buffer是NIO的基本构建块。
    2 提供固定大小的容器来读写数据。
    3 每一个Buffer都是可读的,但是选中的Buffer才可以写。
    4 Buffer是Channel的其中一个端点。
    5 在只读Buffer中数据内容是不可变的,但是Buffer的mark,position,limit是可以变的。
    6 默认Buffer不是线程安全的。

    Buffer类型:
    每一个原生类型都有对应的Buffer类型。所有的Buffer类型都实现了Buffer接口。最常用的Buffer类型是ByteBuffer。在Java NIO包中包含如下的Buffer类型:ByteBuffer,CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer,MappedByteBuffer。

    Buffer Capacity:
    Buffer是固定大小的类型。如果Buffer满了,那么必须先clear,然后才能继续写入。一旦Capacity被设置就不能再改。

    Buffer Limit:
    在写模式中,Buffer的limit和capacity相同。在读模式中,limit是最后写入数据的索引加1。随着Buffer不断写入,Limit会保持增长。0 <= limit <= capacity。

    Buffer Position:
    position指向Buffer的当前位置。当Buffer刚被新建的时候,position指向0,当读或者写的时候,position增加到下一个索引。Position在0和Limit之间。

    Buffer Mark:
    Mark就像对Buffer中的位置做了一个书签。当调用mark()方法的时候,当前的position会被记录。调用reset()方法可以恢复到之前mark的position。

    Buffer flip,clear,rewind:

    flip():调用此方法,使得Buffer准备开始读或者开始一系列新的写。设置limit=position,position=0.
    clear():调用此方法,使得Buffer准备开始写或者启动一系列的新的读。设置limit=capacity,position=0.
    rewind():想要从头开始读时,调用此方法,会把position设成0.

    从Buffer中读数据示例代码:

    ByteBuffer byteBuffer = ByteBuffer.allocate(512);
    byteBuffer.flip(); //Flip the buffer to prepare for read operation.
    int numberOfBytes = fileChannel.read(byteBuffer);
    char c = (char)byteBuffer.get();
    

    向Buffer中写数据:

    ByteBuffer byteBuffer = ByteBuffer.allocate(512);
    byteBuffer.put((byte) 0xff);
    

    在NIO中,如果你要向文件中写数据,先把数据写入到Buffer,然后用Channel把Buffer写入到文件。与此同理,如果要从文件读数据,用Channel把数据从文件读到Buffer,然后从Buffer中取数据。

    package com.javapapers.java.nio;
    
    import java.io.IOException;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    import java.nio.file.StandardOpenOption;
    
    public class BufferExample {
        public static void main(String[] args) throws IOException {
            Path path = Paths.get("temp.txt");
            write(path);
            read(path);
        }
    
        private static void write(Path path) throws IOException {
            String input = "NIO Buffer Hello World!";
            byte[] inputBytes = input.getBytes();
            ByteBuffer byteBuffer = ByteBuffer.wrap(inputBytes);
            FileChannel channelWrite = FileChannel.open(path,
                    StandardOpenOption.CREATE, StandardOpenOption.WRITE);
            channelWrite.write(byteBuffer);
            channelWrite.close();
        }
    
        private static void read(Path path) throws IOException {
            FileChannel channelRead = FileChannel.open(path);
            ByteBuffer byteBuffer = ByteBuffer.allocate(512);
            channelRead.read(byteBuffer);
            byte[] byteArray = byteBuffer.array();
            String fileContent = new String(byteArray).trim();
            System.out.println("File Content: " + fileContent);
            channelRead.close();
        }
    }
    

    本文翻译自 http://javapapers.com/java/java-nio-buffer/,仅供参考和学习。

    Java NIO – Channel

    在Java NIO中,Channel被用来I/O传输。Channel就像一个用来在Buffer和另外一头的实体(比如文件)之间传输数据的管道。一个Channel从一个实体里读取数据,并且把数据放到缓冲区(Buffer)里供别人(一般是我们的程序)消费,类似的,我们应该把数据写入到缓冲区里,然后Channel会把缓冲区里的数据传输到I/O的另一端。

    Channel是Java NIO提供的用来访问原生的I/O机制的一种途径,我们在编程时应该使用Buffer来和Channel交互,所以,Channel更像是I/O两端实体的桥梁。所谓的Buffer,是Channel用来发送和接收数据的端点。

    JavaNIOChannel.png

    Channel的特点:

    • 与传统的流(stream)相比,Channel是双向的,既可以写,也可以读。
    • Channel可以把数据读进Buffer,也可以从Buffer里写数据。
    • Channel可以执行异步(asynchronous)的读写操作。
    • Channel可以支持阻塞(blocking)或者非阻塞(non-blocking)模式。
    • 非阻塞的Channel不会使调用的线程进入sleep模式。
    • 基于流的Channel(比如Socket)只能处于非阻塞模式。
    • 数据可以在Channel和Channel之间传输,前提是其中一个Channel是FileChannel。

    Java NIO中的Channel类
    以下是Java NIO包中提供的两种主要的Channel类实现:

    • FileChannel
      -文件读写的Channel,不支持非阻塞模式。
    • SocketChannel
      -有三种Socket Channel类,包括SocketChannel, ServerSocketChannel and DatagramChannel。
      -可选择(selectable)的Channel,支持非阻塞模式。

    一个Java NIO Channel的例子
    下面这个例子从文本文件中读数据并且把内容打印到控制台中。我们使用RandomAccessFile和FileChannel把数据读取到ByteBuffer。我们读取512字节的数据到buffer,然后调用buffer的flip方法,使其准备好get操作。一旦我们把数据打印到控制台,我们调用buffer的clear方法,使其可以进行下一次读。如此,一直到文件的末尾。

    package com.javapapers.java.nio;
    
    import java.io.IOException;
    import java.io.RandomAccessFile;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    
    public class ChannelExample {
        public static void main(String args[]) throws IOException {
            RandomAccessFile file = new RandomAccessFile("temp.txt", "r");
            FileChannel fileChannel = file.getChannel();
            ByteBuffer byteBuffer = ByteBuffer.allocate(512);
            while (fileChannel.read(byteBuffer) &gt; 0) {
                // flip the buffer to prepare for get operation
                byteBuffer.flip();
                while (byteBuffer.hasRemaining()) {
                    System.out.print((char) byteBuffer.get());
                }
                // clear the buffer ready for next sequence of read
                byteBuffer.clear();
            }
            file.close();
        }
    }
    

    下一篇,我们会详细看下Java NIO的Buffer和各种Channel类。

    本文翻译自 http://javapapers.com/java/java-nio-channel/,仅供参考和学习。