当前位置: 首页 > 工具软件 > TestableMock > 使用案例 >

TestableMock这一篇就够了

堵德曜
2023-12-01

TestableMock简介

TestableMock是基于源码和字节码增强的Java单元测试辅助工具,包含以下功能:

  • 快速Mock任意调用:使被测类的任意方法调用快速替换为Mock方法,实现"指哪换哪",解决传统Mock工具使用繁琐的问题
  • 访问被测类私有成员:使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
  • 辅助测试void方法:利用Mock校验器对方法的内部逻辑进行检查,解决无返回值方法难以实施单元测试的问题
  • 快速构造参数对象:生成任意多层嵌套的对象实例,并简化其内部成员赋值方式,解决被测方法参数初始化代码冗长的问题

快速上手

Maven项目中使用

在项目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>

在Gradle项目中使用

在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}"
            }
        }
    }
}

编码规范

1. 非标准位置的Mock容器类

TestableMock会依次在以下两个位置寻找Mock容器:

  • 默认位置测试类中名为Mock的静态内部类(譬如原类型是Demo,Mock容器类为DemoTest.Mock)
  • 同包路径下名为被测类+Mock的独立类(譬如原类型是Demo,Mock容器类为DemoMock)

倘若实际要使用的Mock容器类不在这两个位置,就需要在测试类上使用@MockWith注释了。一般来说,造成Mock容器类不在默认位置的原因可能有两种:复用Mock容器、集中管理Mock容器

另一种情况是开发者希望将Mock方法的定义与测试类本身分开,以便进行集中管理(或规避某些扫描工具的路径规则),譬如形成下面这种目录结构:

src/
  main/
    com/
      demo/
        service/
          DemoService.java
  test/
    com/
      demo/
        service/
          DemoServiceTest.java
      mock/
        service/
          DemoServiceMock.java

此时需要在测试类上显式的指定相应的Mock容器类,不过这种情况在实际中并不太常见。

2. 非标准位置的测试类

从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 {
    ...
}

3. 在一个测试类中测试多个被测类

这是非标准位置测试类的一种特殊情况,当一个测试类里同时测试了多个业务类(被测类),其名称要么只能与其中某个被测类有+Test的命名符合,要么不与其中任何一个被测类有命名相关性。

假设所有被测类的Mock容器均采用被测类+Mock约定命名(否则参考前一条规则,被测类也需要显式加@MockWith)。若该测试类本身命名不符合其中任何一个被测类+Test约定的情况,需要为该测试类加一个无参数的@MockWith注解(即使用默认值,相当于@MockWith(NullType.class)),用于标识此类需参与TestableMock的预处理。

4. 使用不包含Mock方法的Mock容器类

为了加快搜索Mock容器类的速度,在扫描过程中,TestableMock只会将自身定义有Mock方法(包含@MockMethod或@MockMockConstructor注解的方法)以及明确被@MockWith指向的类识别为有效的Mock容器,而不会去遍历其父类。

假如出于某些极特殊原因要使用无Mock方法的类型作为Mock容器,譬如希望将实际Mock方法均定义在父类,实际使用的子容器仅仅重载父类的某些特定方法。此时即使Mock容器类的位置符合约定,为了能够被识别,依然应该在相应的测试类上增加对Mock容器类的@MockWith引用。

注意事项:

  1. 编写测试类时注意类的路径要和被测试类保持一致;
  2. 当测试类与被测试类不是按照"测试类+Test"的规则命名时需要使用@MockWith注解进行关联;
  3. 初始化被测类中通过@Autowired或@Resource注入的私有字段时若该对象的方法在测试时需要被Mock,则无需初始化,其他情况可以手动new或依赖spring注入

设计与原理

与常见的Mock工具在每个测试用例里写Mock定义不同,TestableMock让每个业务类直接提供自己的Mock方法集合,描述自身在测试时需要被Mock的调用以及相应替代逻辑(即每个业务类有自己的独立Test类和独立Mock类)。采用约定优于配置,降低Mock学习理解成本、减少冗余信息。

这种设计基于两项基本假设:

  1. 同一个测试类里,一个测试用例里需要Mock掉的方法,在其他测试用例里通常也都需要Mock。因为这些被Mock的方法往往访问了不便于测试的外部依赖。
  2. 需要Mock的调用都来自被测类的代码。(此假设是符合单元测试初衷的,即单元测试只应该关注当前单元的内部行为,单元外的逻辑应该被替换为Mock)

据此通过约定来简化符合该假设的单元测试场景,通过配置来支持其余复杂的使用场景。

TestableMock的原理可以用一句话概括:利用JavaAgent动态修改字节码,把被测的业务类中与所有与Mock方法定义匹配的调用在单元测试运行时替换成对Mock方法的调用

最终达到的效果则是,不论代码用什么服务框架、什么对象容器,不论要Mock的目标对象是注入的、new出来的、全局的还是局部的,不论要Mock的目标方法是私有的、外部的、静态的、继承来的或者重载过的,全部无差别通吃,让单元测试回归简单。

主流框架对比

TestableMock外,目前主要的Mock工具主要有MockitoSpockPowerMockJMockit,基本差异如下:

工具原理最小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是一款功能性与易用性均居于MockitoPowerMock之间的Mock工具,较好的弥补了两者各自的不足。该项目在2017年尝试推出JMockit2重写版本但未能完成,目前处于不活跃的维护状态。

相比之下,TestabledMock的功能与PowerMock基本平齐,且极易上手,只需掌握@MockMethod注解就可以完成绝大多数任务。

当前TestableMock的主要不足在于,编写Mock方法时IDE尚无法即时提示方法参数是否正确匹配。若发现匹配效果不符合预期,需要通过自助问题排查文档提供的方法在运行期进行校验。这个功能未来需要通过扩展主流IDE插件来提供。

此外,由于TestableMock独辟蹊径的采用基于单个方法的Mock机制,将Mock方法定义与单元测试用例解耦,一方面使得Mock方法具有默认可复用性,单元测试用例也因此变得更干净纯粹,另一方面也导致Mock方法定义变得零散,生命周期管理起来相对困难,对现有开发者的Mock编写习惯会带来一定改变。

 

 类似资料: