springboot集成mockito和jacoco实践

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,因此不需要额外引入。

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>3.5.0</version>
</dependency>

jacoco依赖

jacoco是用来计算代码覆盖率的,小型项目里常用的框架,如果是大规模项目就得用别的了,比如 SonarQube/SonarCloud。

xml 复制代码
<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 要做的事。

之所以说"如果不想钻牛角尖",是因为我自己就思路打结过,毕竟单元可大可小,各功能之间又是调用来调用去的......

单元测试示例

单元测试与集成测试最直观的区别,在我看来就是类层面的注解。

java 复制代码
//单元测试
@ExtendWith(MockitoExtension.class)
//集成测试
@ExtendWith(SpringExtension.class)
@SpringBootTest

正如前面表格所述,mockito 的作用是模拟依赖对象(如 Service、DAO),验证交互行为(如 verify),因此测试时,基本都是要假定输入与输出,再去执行。

以 controller 层的测试为例,测试的是 controller 的接口,但由于 controller 大多数时候调用了 service,所以还需要一个虚拟的 service,用来模拟不同的 service 返回结果在 controller 是否得到了正确处理。

简单写一个用于校验的 service,有多简单?直接返回 Boolean 结果。

java 复制代码
@Service
public class DemoServiceImpl implements DemoService {
    
    @Override
    public boolean jiaoyanTest(){
        return true;
    }

controller 里调用它,并对不同的返回结果做处理------打印不同字符串。

java 复制代码
@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;
    }

}

在单元测试类里,就需要这么测试它。

java 复制代码
@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 断言都正常通过------输入依照设定,输出符合预期,有什么理由不通过呢?

console 复制代码
jianquan执行结果:通过
jianquan执行结果:不通过

如果颠倒断言的顺序而保持 mock 设定的顺序不变,这个逻辑必然出错。

console 复制代码
jianquan执行结果:通过

org.opentest4j.AssertionFailedError: 
Expected :不通过
Actual   :通过
<Click to see difference>

执行结果也忠实地打印了实际效果:验证不通过,并且不再执行下一步校验(毕竟是故意写的错误逻辑)。第一行是 controller 内的打印结果,后面是断言的提示:期望返回应是"不通过",但实际是"通过",说明执行结果与期望不符。

集成测试示例

和平常启动项目时的写法基本一致。

less 复制代码
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class DemoControllerTest {
    @Autowired
    private DemoController demoController;

    @Test
    void test4(){
        // 调用真实的服务方法
        String res = demoController.jianquan();
        // 根据实际业务逻辑验证结果
        assertEquals("不通过", res); //
    }
}

总结

主要方便自己忘了的时候找回写法(。

相关推荐
会算数的⑨1 分钟前
Kafka知识点问题驱动式的回顾与复习——(一)
分布式·后端·中间件·kafka
Hx_Ma166 分钟前
SSM搭建(三)Spring整合SpringMVC框架
java·后端·spring
William_cl14 分钟前
ASP.NET路由长度约束精讲:[HttpGet (“{name:minlength (3)}“)] 字符长度限制吃透,附避坑指南 + 实战代码
后端·asp.net
我命由我1234524 分钟前
Java 泛型 - Java 泛型通配符(上界通配符、下界通配符、无界通配符、PECS 原则)
java·开发语言·后端·java-ee·intellij-idea·idea·intellij idea
szhf7824 分钟前
SpringBoot Test详解
spring boot·后端·log4j
无尽的沉默24 分钟前
SpringBoot整合Redis
spring boot·redis·后端
摸鱼的春哥31 分钟前
春哥的Agent通关秘籍07:5分钟实现文件归类助手【实战】
前端·javascript·后端
Victor3561 小时前
MongoDB(2)MongoDB与传统关系型数据库的主要区别是什么?
后端
JaguarJack1 小时前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端·php·服务端