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); //
    }
}

总结

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

相关推荐
风象南几秒前
SpringBoot基于Java Agent的无侵入式监控实现
java·spring boot·后端
崎岖Qiu7 分钟前
【Spring篇08】:理解自动装配,从spring.factories到.imports剖析
java·spring boot·后端·spring·面试·java-ee
香饽饽~、1 小时前
【第十一篇】SpringBoot缓存技术
java·开发语言·spring boot·后端·缓存·intellij-idea
程序员爱钓鱼2 小时前
Go语言实战指南 —— Go中的反射机制:reflect 包使用
后端·google·go
ℳ₯㎕ddzོꦿ࿐2 小时前
Spring Boot 集成 MinIO 实现分布式文件存储与管理
spring boot·分布式·后端
ai小鬼头7 小时前
百度秒搭发布:无代码编程如何让普通人轻松打造AI应用?
前端·后端·github
考虑考虑7 小时前
@FilterRegistration和@ServletRegistration注解
spring boot·后端·spring
一只叫煤球的猫7 小时前
🔥 同事混用@Transactional和TransactionTemplate被我怼了,三种事务管理到底怎么选?
java·spring boot·后端
你的人类朋友9 天前
(●'◡'●)从Dockerfile快速入门Docker Compose
后端
GetcharZp9 天前
「神器推荐」Rclone:轻松玩转云端存储,FTP 也能飞起来!
后端