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/

    clone github repo提示无权限

    今天要从GitHub clone一个代码库:

    git clone git@github.com:gshine/exampleofjsonrpc4j.git

    提示报错:

    git@github.com: Permission denied (publickey).
    fatal: Could not read from remote repository.
    
    Please make sure you have the correct access rights
    and the repository exists.
    
    

    查看了下已经把公钥上传到了github(如果你之前没配置过,可以参考这里生成并配置密钥。)

    后来从网上查了下,执行了下面这个命令就好了。

    ssh-add ~/.ssh/***_rsa
    

    其中”***_rsa”换成你自己的密钥文件。配置完后可以通过

    ssh -T git@github.com

    来测试下联通性。

    很奇怪,之前已经配置好的竟然突然出现这个问题,怀疑可能是ssh版本升级导致的。

    总结下出现此问题,可以从以下几个方面来排查:

    • SSH key是否配置正确,参考此链接
    • 密钥是否加到key-chain里,此问题可以通过ssh-add来解决。
    • 本地是否有多个ssh密钥对,如果是的话,需要在~/.ssh/config里面针对不同的host配置不同的私钥。参考此链接
    • 可能代码库不存在

    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

     

    转换dmesg中时间戳的方法

    如果发现线上tomcat服务器进程突然没有了(可以通过ps aux | grep java来确认),一种可能是进程被内核给干掉了。

    此时在命令行执行dmesg,可以看到类似信息:

    [84368070.456270] Out of memory: Kill process 15166 (java) score 621 or sacrifice child
    [84368070.456304] Killed process 15166, UID 59846, (java) total-vm:4077904kB, anon-rss:2515140kB, file-rss:612kB

    这种情况,一般是服务器内存不足,然后内核把占内存最大的进程给杀掉了。

    在上面这个信息中,最前面方括号里面的就是杀死进程的时间戳,这个数字84368070.456304的意思代表内核自从启动到现在经过的秒数,小数点后面精确到纳秒。这个数字很不直观,因此,需要转换成可读的时间表示。

    方法1:(-T不是所有版本都支持,如果不支持,用方法2手动计算)

    dmesg -T

    方法2:

    转换的思路很简单,[当前时间] – [内核启动后经过的秒数] + [dmesg的时间秒数]

    date -d "1970-01-01 UTC `echo "$(date +%s)-$(cat /proc/uptime|cut -f 1 -d' ')+84368070.456304" | bc`seconds"

    具体执行时,把84368070.45630换成你dmesg出来的时间戳即可。

    也可以把以下代码放到文本文件里,作为校本方便以后使用。

    date -d "1970-01-01 UTC `echo "$(date +%s)-$(cat /proc/uptime|cut -f 1 -d' ')+$1" | bc`seconds"

    比如存为showDmesgTimestamp.sh,则可以直接执行

    sh showDmesgTimestamp.sh 84368070.45630

    即可打印可读形式的时间表示。

    一个好用的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

    win10运行Clojure脚本

    像Python一样,用Clojure写一些日常要用的小脚本。

    1)常规方法
    前提:安装了JDK,并下载了clojure的jar包。
    因为clojure本身是基于JVM的,而且它本身集成在一个jar包里,因此,执行clojure脚本可以直接通过java命令。
    如下:

    java -cp /path/to/clojure.jar clojure.main file1.clj arg1 arg2

    在file1.clj中,可以通过*command-line-args*得到参数的list,比如(arg1, arg2)。

    2)使用leiningen的插件lein-exec
    前提:安装了JDK(v8.0)和Leiningen(v2.8.0)。
    在~/.lein/profiles.clj(如果不存在,则创建此文件)中添加:

    {:user {:plugins [[lein-exec "0.3.6"]]}}

    然后在命令行执行lein version 或者其他的task,那么就会自动下载插件(默认下载的~/.m2/repository/lein-exec/目录下)。
    此步如果卡着不动,可能是网络原因,可以从:https://mvnrepository.com/artifact/lein-exec/lein-exec/0.3.6 手动下载下来,放到本地对应目录下。

    之后就可以通过

    lein exec file1.clj arg1 arg2

    来执行脚本文件了。在file1.clj中,可以通过*command-line-args*得到参数的list,但是与上面的常规方法不一样的是:它是包含脚本文件的名字的,比如(file1.clj, arg1, arg2)。

    参考:
    https://github.com/kumarshantanu/lein-exec
    https://clojure.github.io/clojure/clojure.main-api.html

    Guava的设计哲学

    本文是自己学习Guava类库的一些学习心得,主要来自Guava的官方文档,并夹杂了一些自己的理解。
    Guava是Google内部使用的核心Java类库的开源版本,这些核心实用类都是Google内部员工平时编码中经常使用的类库,已经在Google的生产环境验证,因此,我们可以放心的使用他们。
    Guava的定位是使得用Java语言工作更加愉快和更加高效。JDK自带的utilities类(比如Collections类)被广泛应用并极大的简化了Java代码,Guava的目标就是要继续这个“传说”。
    Effective Java第47条,“了解并使用类库”,告诉我们,使用类库比自己写一个实用工具类更可取。(个人觉得,如果为了学习,这条可以忽略,相反,自己写一个类库并且跟现有的优秀的类库比较,反而更利于个人成长。)
    【总之,不要重复造轮子。如果你需要一些看起来很通用的功能,那么可能已经存在相应的类库实现了你的功能。如果存在,那么就使用它;如果你不知道,那么可以去搜下。总之,使用类库大部分情况比自己实现要更好。这个无关你作为一个程序员的能力,因为通用的类库集聚了大部分程序员的努力,肯定比你自己实现这个功能投入的精力要多。】(摘自effective java)
    所以,不得不说:
    • Guava在Google的生产环境经过了实战测试。
    • Guava经过了很多的单元测试。自动化的测试,并且覆盖率很高,尤其是collect子包。
    • Guava开发很活跃,用户参与也很积极。
    • 总之,很牛逼。
    Guava在增加特性方面偏保守,在决定是否增加一个特性时,会考虑是否‘实用’并且‘普遍存在’。‘实用’表现在:可以节省你大量的代码;避免让你写出很难调试或者易于出错的代码;提高可读性;提高速度。‘普遍存在’就是在代码中竟然被使用,这一项可以通过Google内部的代码库里扫描统计出来。
    相对于增加特性方面的保守,Guava在去除不需要的特性方面很果断,会把这些特性标为deprecate继而删除该特性。因此,使用@Beta注解的类或者方法要谨慎使用。
    Guava的实现针对一些通用的设计原则,包括:
    • 针对特定的场景,这些api是最优的解决方案;
    • 类或者方法的语义是可以从他们的签名上很明显很直观的看出来。实现可以很smart,但是名字要很直观。
    • 鼓励Guava的用户有好的代码习惯,而Guava它自己的代码本身也是好代码习惯的典范。(比如快速失败,拒绝null)
    • 不要试图单独解决每一种场景,相反提供通用的工具以解决我们可能没遇到过的场景。
    • 强调可维护性,并留出未来重构的空间。(结果:大部分暴露出去的类应该是final的,就像effective java 17条说的;把AbstractXXX类暴露出去也应该很谨慎。)

    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