mockito,一款 spring 框架开发中常用的单元测试工具,junit亦然。在实践中二者经常混合使用,各司其职,共同完成测试任务。核心关系和分工如下:
工具 | 职责 | 在之前代码中的体现 |
JUnit | 提供测试框架,管理测试生命周期(如 @Test、@BeforeEach),断言验证(如 assertEquals)。 | 1. 标记测试方法 (@Test) 2. 断言结果 (assertEquals) 3. 测试类的结构(如 class XxxTest)。 |
Mockito | 模拟依赖对象(如 Service、DAO),验证交互行为(如 verify)。 | 1. 模拟 demoService (@Mock) 2. 验证方法调用 (verify(demoService).getDto())。 |
通常单元测试做完后还需要 “单测覆盖率” 的数据,类似在校时各类大小考试后的卷面评分,这个环节一般使用 jacoco。大型项目可能会使用 SonarQube(听说的,实际有没有我也不知道),这个不在此次讨论范围内。
本文开发版本前提:
JDK | spring-boot | spring-cloud | spring-cloud-alibaba |
1.8 | 2.7.18 | 2021.0.9 | 2021.0.6.1 |
mockito 和 junit 依赖
官网:site.mockito.org/
微服务开发下的mockito已经集成在了spring-boot-start-test模块里,包括junit,因此不需要额外引入。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>3.5.0</version>
</dependency>
jacoco依赖
jacoco是用来计算代码覆盖率的,小型项目里常用的框架,如果是大规模项目就得用别的了,比如 SonarQube/SonarCloud。
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version> <!-- 使用最新版本 -->
<executions>
<execution>
<goals>
<goal>prepare-agent</goal> <!-- 在测试阶段注入 JaCoCo 代理 -->
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase> <!-- 在 test 阶段生成报告 -->
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
安装完后用 mvn clean test 在项目根目录下执行就能得到测试报告(前提是已经有单元测试类及代码)
如果要单独指定某一个子模块并屏蔽其他模块,可以用mvn clean test -pl module-name -am。最终生成的报告在目标模块下的 target/site/jacoco 文件夹里。
单元测试与集成测试
如果不想钻牛角尖的话,单元测试可以理解为多程序逻辑中各个“最小单元”的测试,这些最小单元可以被预设输入值和期望输出值,可以验证,这也就是 mockito 要做的事。
之所以说“如果不想钻牛角尖”,是因为我自己就思路打结过,毕竟单元可大可小,各功能之间又是调用来调用去的……
单元测试示例
单元测试与集成测试最直观的区别,在我看来就是类层面的注解。
//单元测试
@ExtendWith(MockitoExtension.class)
//集成测试
@ExtendWith(SpringExtension.class)
@SpringBootTest
正如前面表格所述,mockito 的作用是模拟依赖对象(如 Service、DAO),验证交互行为(如 verify),因此测试时,基本都是要假定输入与输出,再去执行。
以 controller 层的测试为例,测试的是 controller 的接口,但由于 controller 大多数时候调用了 service,所以还需要一个虚拟的 service,用来模拟不同的 service 返回结果在 controller 是否得到了正确处理。
简单写一个用于校验的 service,有多简单?直接返回 Boolean 结果。
@Service
public class DemoServiceImpl implements DemoService {
@Override
public boolean jiaoyanTest(){
return true;
}
controller 里调用它,并对不同的返回结果做处理——打印不同字符串。
@RestController
public class DemoController {
@Autowired
private DemoService demoService;
@GetMapping(PREFIX+"/jianquan")
public String jianquan(){
String res = "通过";
if (!demoService.jiaoyanTest()){
res = "不通过";
}
System.out.println("jianquan执行结果:"+res);
return res;
}
}
在单元测试类里,就需要这么测试它。
@Mock
private DemoService demoService;
@InjectMocks
private DemoController demoController;
@Test
void test(){
//模拟service返回true
Mockito.when(demoService.jiaoyanTest()).thenReturn(true);
String res = demoController.jianquan();
assertEquals("通过",res);
//模拟service返回false
Mockito.when(demoService.jiaoyanTest()).thenReturn(false);
res = demoController.jianquan();
assertEquals("不通过",res);
}
执行结果就会是两个 assert 断言都正常通过——输入依照设定,输出符合预期,有什么理由不通过呢?
jianquan执行结果:通过
jianquan执行结果:不通过
如果颠倒断言的顺序而保持 mock 设定的顺序不变,这个逻辑必然出错。
jianquan执行结果:通过
org.opentest4j.AssertionFailedError:
Expected :不通过
Actual :通过
<Click to see difference>
执行结果也忠实地打印了实际效果:验证不通过,并且不再执行下一步校验(毕竟是故意写的错误逻辑)。第一行是 controller 内的打印结果,后面是断言的提示:期望返回应是“不通过”,但实际是“通过”,说明执行结果与期望不符。
集成测试示例
和平常启动项目时的写法基本一致。
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class DemoControllerTest {
@Autowired
private DemoController demoController;
@Test
void test4(){
// 调用真实的服务方法
String res = demoController.jianquan();
// 根据实际业务逻辑验证结果
assertEquals("不通过", res); //
}
}
总结
主要方便自己忘了的时候找回写法。