TestableMock是基于源码和字节码增强的Java单元测试辅助工具,包含以下功能:
在项目pom.xml文件中,增加testable-all依赖和maven-surefire-plugin配置,具体方法如下。
在dependencies列表添加TestableMock依赖:
<dependencies>
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-all</artifactId>
<version>0.5.1</version>
<scope>test</scope>
</dependency>
</dependencies>
最后在build区域的plugins列表里添加maven-surefire-plugin插件(如果已包含此插件则只需添加<argLine>部分配置):
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
</configuration>
</plugin>
</plugins>
</build>
若项目同时还使用了Jacoco的on-the-fly模式(默认模式)统计单元测试覆盖率,则需在<argLine>配置中添加一个@{argLine}参数,添加后的配置如下:
<argLine>@{argLine} -javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/${testable.version}/testable-agent-${testable.version}.jar</argLine>
在build.gradle文件中添加TestableMock依赖:
dependencies {
testImplementation('com.alibaba.testable:testable-all:0.5.1')
testAnnotationProcessor('com.alibaba.testable:testable-processor:0.5.1')
}
然后在测试配置中添加javaagent:
test {
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
}
若是基于Robolectric框架的Android项目,则添加TestableMock依赖方法同上,添加javaagent配置方法如下:
android {
testOptions {
unitTests {
all {
jvmArgs "-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}"
}
}
}
}
TestableMock会依次在以下两个位置寻找Mock容器:
倘若实际要使用的Mock容器类不在这两个位置,就需要在测试类上使用@MockWith注释了。一般来说,造成Mock容器类不在默认位置的原因可能有两种:复用Mock容器、集中管理Mock容器
另一种情况是开发者希望将Mock方法的定义与测试类本身分开,以便进行集中管理(或规避某些扫描工具的路径规则),譬如形成下面这种目录结构:
src/
main/
com/
demo/
service/
DemoService.java
test/
com/
demo/
service/
DemoServiceTest.java
mock/
service/
DemoServiceMock.java
此时需要在测试类上显式的指定相应的Mock容器类,不过这种情况在实际中并不太常见。
从TestableMock的原理来说,测试类的位置其实只是作为“被测类”与“Mock容器类”之间建立关联的参照物。当测试类的位置不是默认约定的被测类+Test时,上述首选的Mock容器位置就不成立了。但此时次选Mock位置依然可用,即如果Mock容器类的位置是被测类+Mock,那么Mock置换就依然能够正常进行。
但此时测试类与Mock容器之间的关联丢失了,因此需要为测试类使用@MockWith注解来显式的建立关联。例如:
public class DemoService { // 被测类
...
}
public class DemoServiceMock { // Mock容器类
...
}
@MockWith(DemoServiceMock.class) // 测试类由于丢失与Mock容器的关联,需要@MockWith注解
public class ServiceTest {
...
}
这是非标准位置测试类的一种特殊情况,当一个测试类里同时测试了多个业务类(被测类),其名称要么只能与其中某个被测类有+Test的命名符合,要么不与其中任何一个被测类有命名相关性。
假设所有被测类的Mock容器均采用被测类+Mock约定命名(否则参考前一条规则,被测类也需要显式加@MockWith)。若该测试类本身命名不符合其中任何一个被测类+Test约定的情况,需要为该测试类加一个无参数的@MockWith注解(即使用默认值,相当于@MockWith(NullType.class)),用于标识此类需参与TestableMock的预处理。
为了加快搜索Mock容器类的速度,在扫描过程中,TestableMock只会将自身定义有Mock方法(包含@MockMethod或@MockMockConstructor注解的方法)以及明确被@MockWith指向的类识别为有效的Mock容器,而不会去遍历其父类。
假如出于某些极特殊原因要使用无Mock方法的类型作为Mock容器,譬如希望将实际Mock方法均定义在父类,实际使用的子容器仅仅重载父类的某些特定方法。此时即使Mock容器类的位置符合约定,为了能够被识别,依然应该在相应的测试类上增加对Mock容器类的@MockWith引用。
注意事项:
与常见的Mock工具在每个测试用例里写Mock定义不同,TestableMock
让每个业务类直接提供自己的Mock方法集合,描述自身在测试时需要被Mock的调用以及相应替代逻辑(即每个业务类有自己的独立Test类和独立Mock类)。采用约定优于配置,降低Mock学习理解成本、减少冗余信息。
这种设计基于两项基本假设:
据此通过约定来简化符合该假设的单元测试场景,通过配置来支持其余复杂的使用场景。
TestableMock
的原理可以用一句话概括:利用JavaAgent动态修改字节码,把被测的业务类中与所有与Mock方法定义匹配的调用在单元测试运行时替换成对Mock方法的调用。
最终达到的效果则是,不论代码用什么服务框架、什么对象容器,不论要Mock的目标对象是注入的、new出来的、全局的还是局部的,不论要Mock的目标方法是私有的、外部的、静态的、继承来的或者重载过的,全部无差别通吃,让单元测试回归简单。
除TestableMock
外,目前主要的Mock工具主要有Mockito
、Spock
、PowerMock
和JMockit
,基本差异如下:
工具 | 原理 | 最小Mock单元 | 对被Mock方法的限制 | 上手难度 | IDE支持 |
---|---|---|---|---|---|
Mockito | 动态代理 | 类 | 不能Mock私有/静态和构造方法 | 较容易 | 很好 |
Spock | 动态代理 | 类 | 不能Mock私有/静态和构造方法 | 较复杂 | 一般 |
PowerMock | 自定义类加载器 | 类 | 任何方法皆可 | 较复杂 | 较好 |
JMockit | 运行时字节码修改 | 类 | 不能Mock构造方法(new操作符) | 较复杂 | 一般 |
TestableMock | 运行时字节码修改 | 方法 | 任何方法皆可 | 很容易 | 一般 |
Mockito
是Java最老牌的Mock工具,稳定性和易用性较好,IntelliJ和Eclipse都有专用插件支持。相对不足之处在于Mock功能稍弱,在必要情况下需与其他Mock工具配合使用。
Spock
是一款代码可读性非常高的单元测试框架,内置Mock支持,具有很好的整体感。由于同样基于动态代理实现,其不足点与Mockito
类似。
PowerMock
是一款功能十分强大的Mock工具,其基本语法与Mockito
兼容,同时扩展了许多Mockito
缺失的功能,包括对支持对私有、静态和构造方法实施Mock。但由于使用了自定义类加载器,会导致Jacoco在默认的on-the-fly
模式下覆盖率跌零。
JMockit
是一款功能性与易用性均居于Mockito
与PowerMock
之间的Mock工具,较好的弥补了两者各自的不足。该项目在2017年尝试推出JMockit2重写版本但未能完成,目前处于不活跃的维护状态。
相比之下,TestabledMock
的功能与PowerMock
基本平齐,且极易上手,只需掌握@MockMethod
注解就可以完成绝大多数任务。
当前TestableMock
的主要不足在于,编写Mock方法时IDE尚无法即时提示方法参数是否正确匹配。若发现匹配效果不符合预期,需要通过自助问题排查文档提供的方法在运行期进行校验。这个功能未来需要通过扩展主流IDE插件来提供。
此外,由于TestableMock
独辟蹊径的采用基于单个方法的Mock机制,将Mock方法定义与单元测试用例解耦,一方面使得Mock方法具有默认可复用性,单元测试用例也因此变得更干净纯粹,另一方面也导致Mock方法定义变得零散,生命周期管理起来相对困难,对现有开发者的Mock编写习惯会带来一定改变。