静态方法,mock 还是不 mock,这是个问题

王者 Mockito

不知从何时开始,Mockito 成了 Java 的单元测试框架王者,目前(2019年7月)Github 上 star 数直逼 10K。看看其他的单元测试工具:PowerMock 2K(无疑是沾了 Mockito 的光),easymock 600,JMockit 300。跟 Mockito 一比,好可怜啊,一个能打的都没有。

Mockito 当然很好。我从2012年还是2013年开始用 Mockito,看着它从 1.0x 版本一路走来,今年晚些时候估计会正式发布 3.0 版本。应该有不少人都跟我有类似的体验,从 Mockito 开始接触 mock / stub,一边赞叹 Mockito 语法的简练,一边享受着 mock 带来的单元测试的便利性。总说单元测试应该要隔离外部依赖和实现,很难想象,如果没有 mock,怎么写单元测试呢?

public void test() {
    when(userDao.update(any(User.class))).thenReturn(1);
    int actual = userService.update(aUser);
    Assert.assertTrue(acutal > 0);
    verify(userDao).update(aUser);
}

看看上面这个 Mockito 的例子,when(...).thenReturn(...)verify(...).doSomething(),这代码就像人类语言,多么简明易懂!

但是(没错转折来了),已经2019年了,Mockito 依然不支持 mock 静态方法、构造方法等。你可以说,这是设计理念,Mockito 首页上一直写着一句话 "Don’t mock everything" ,认为说应该做好功能代码的设计,尽量避免静态方法等,尽量使你的代码易于测试。这个理念,在理论上没问题,但这么多年的开发经验告诉我,理想归理想,实际上要你去维护的遗留代码总是一箩筐一箩筐的,避无可避。

Static methods, to mock or not to mock, that is the question

单元测试中是否要 mock 静态方法,一直争论不休,网上有 一个 一个 又一个 的讨论,各种意见都有。

我的个人意见,跟 这个观点 一样,我认为测试工具不应该替用户决定什么是好、什么是不好,而应该尽量提供选择,让用户自行判断、采取合适的方案。理论很美好,但实际情况就是,google 搜 "mockito how to mock static methods",有近15万条结果,可想而知,全世界的开发者在这个问题上浪费了多少时间。

真要用 Mockito 来 mock 静态方法,一般都是结合 PowerMock 使用。这两年 PowerMock 发展的怎么样我不太清楚,但14、15年那会儿我用过 PowerMock,感受就是,真他妈累啊!理论上来说是可以的,但实际做起来就总是各种问题,然后各种 google 、解决,然后又继续各种问题,排查的我都快怀疑人生了。最终我是放弃了 PowerMock 的,这么费力地去结合两个工具一起用,往后很难说还有多少坑。

Mockito、EasyMock 等工具不支持 mock 静态方法,原理上是因为它们都是基于 cglib 的,只能通过创建子类或实现接口的方式去 mock。那除了 cglib ,就没有其他的 mock 实现方法了吗?当然有,修改字节码呀!

另辟蹊径的 JMockit

和其他大多数使用 cglib 实现的单元测试工具不同,JMockit 使用 JDK6 的 java.lang.instrument 包和 ASM,动态地在运行时修改字节码,从而实现 "Mock Anything" 。什么静态方法、构造函数,随时随地想 mock 就 mock。一个 JMockit ,解决了 Mockito + PowerMock 两个工具都解决不了的问题,那为啥不用 JMockit 呢?JMockit 为啥流行不起来呢?

public class UserServiceTest {

    @Tested
    private UserService userService;
    @Injectable
    private UserDao userDao;

    public void test() {
        new Expectations() {
            {
                userDao.update(withInstanceOf(User.class));
                result = 1;
            }
        };

        int actual = userService.update(aUser);
        Assert.assertTrue(acutal > 0);

        new Verifications() {
            {
                userDao.update(withInstanceOf(User.class));
            }
        };
    }
}

功能更强大的 JMockit 却流行不起来,我觉得其中一个原因,是它的语法不太友好。看看上面这个 JMockit 的例子,这坨 new Expectations(){...}new Verifications(){...} 是什么鬼?匿名类?为啥里面又有一层大括号?别说测试代码了,在普通的功能代码中,我们都极少见到这样的语法。多数人可能觉得不习惯,然后就此打住,放弃 JMockit 了。

JMockit 的这种语法,是基于它的 record-replay-verify 模型。new Expectations() 是录制期望,new Verifications() 是校验,二者中间的就是回放——正常调用业务方法。而在匿名内部类类中间的那层大括号,是 Java 的“实例初始化块” (Instance Initialization Blocks),我们平时可能用“静态初始化块”比较多,“实例初始化块”确实较少见,它的其中一种用途,就是用来初始化匿名内部类,因为匿名内部类不能有构造函数。理解了这些语法之后,其实 JMockit 不难懂,用法跟其他测试框架也大致一样,就是功能更强大了。

JMockit 不够流行的另一个原因,我猜可能跟社区有关。没办法,Mockito 太受欢迎了,社区一片火热,贡献者一大堆。反观 JMockit,虽然开源,但只有原作者 Rogério Liesenfeld 自己一个人在开发维护。这种单人维护的项目,说不定哪一天就停更了,大家都会有这种担忧。我也担心啊,但看看近几年 JMockit 的 release notes,基本上固定每一、两个月一次发布,并且还会提前订好下一次发布的计划,真想对作者说一句:老哥,稳!所以,至少目前看来,JMockit 的稳定性、活跃性是不用担心的,毕竟有个这么稳的作者。

JUnit5 + JMockit + Surefire + Jacoco 的配置例子

想要安心用上 Junit5 和 JMockit,还想要单元测试覆盖率?那还是有些坑要踩的。以 Maven 为例,有几个留意点:

  • 默认的 maven-surefire-plugin (运行单元测试的) 的版本不支持 JUnit5,得手动指定新版本号才行。
  • 想用 jacoco-maven-plugin 得到单元测试覆盖率的话,因为 jacoco 也用了修改字节码的方案,默认配置下会和 JMockit 有冲突。需要一些额外配置才行,具体参考下面的例子。

完整的 Maven 配置例子:



    4.0.0

    io.github.renial
    java-utils
    1.0.0
    jar
    java-utils

    
        UTF-8
        UTF-8
        1.8
        1.8

        1.46
        5.4.2 
        2.22.2 
        0.8.4
    

    
        
            org.jmockit
            jmockit
            ${jmockit.version}
            test
        
        
            org.junit.jupiter
            junit-jupiter
            ${jupiter.version}
            test
        
    

    
        
            
                org.apache.maven.plugins
                maven-surefire-plugin
                ${surefire.version}
                
                
                
                    
                        -javaagent:${settings.localRepository}/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar -javaagent:"${settings.localRepository}"/org/jacoco/org.jacoco.agent/${jacoco.version}/org.jacoco.agent-${jacoco.version}-runtime.jar=destfile=${project.build.directory}/jacoco.exec
                    
                
            

            
                org.jacoco
                jacoco-maven-plugin
                ${jacoco.version}
                
                    
                    
                    
                    
                    
                        
                            prepare-agent
                        
                    
                    
                        report
                        test
                        
                            report
                        
                    
                
            
        
    

你可能感兴趣的:(jmockit,mockito,单元测试,java)