别再盲目跑测试了,用 JaCoCo 告诉你哪些代码根本没被覆盖

关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言

之前看到很多文章提到代码覆盖率的问题,但是待在小公司,这些问题总是被弱化,也没有真正关注过到代码覆盖率的问题。代码覆盖率是衡量测试充分性的重要指标。它不能直接证明代码质量高,但覆盖率低,代码质量一定好不到哪去。

这两天看到一个框架用来测试代码覆盖率:JaCoCo,特地分享给大家。

02 简介

JaCoCoJava Code Coverage Library)是一款开源的‌Java 代码覆盖率工具,主要用于检测单元测试对代码的覆盖程度 。它通过字节码插桩技术收集执行数据,生成可视化报告,帮助开发者识别未测试的代码路径,是 Java 生态中覆盖率检测的事实标准 。‌‌‌

JaCoCo是当前 Java 生态中最主流的开源代码覆盖率工具,由 EclEmma 团队开发维护,支持 MavenGradleAnt 等构建工具,也能通过 Java Agent 技术在线监控运行中的JVM 程序。

官方地址:www.jacoco.org/jacoco/

GitHub地址:github.com/jacoco/jaco...

03 实战案例

3.1 Maven依赖

xml 复制代码
<build>
    <plugins>
        <plugin>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
            <version>${jacoco.version}</version>
            <executions>
                <execution>
                    <id>prepare-agent</id>
                    <goals>
                        <!-- prepare-agent: 准备 JaCoCo agent -->
                        <!-- 实际效果等同于 JVM 参数: -javaagent:jacocoagent.jar -->
                        <goal>prepare-agent</goal>
                    </goals>
                </execution>
                <execution>
                    <id>report</id> 
                    <phase>test</phase>
                    <goals>
                        <!-- report: 生成覆盖率报告 -->
                        <goal>report</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

该依赖需以插件的形式引入,用于编译期。

在配置中包含需要参数,如下:

其中参数中比较重要的参数:

  • prepare-agent 插件运行的第一阶段,在 process-test-classes 阶段自动执行 。主要用来配置 JaCoCo 运行时 Agent,设置 JVM 参数,对测试类进行字节码插桩,以便收集覆盖数据
  • report:插件运行的第二阶段,在 test 阶段之后执行,主要用来读取 jacoco.exec 数据,生成可读的覆盖率报告

3.2 业务用例

控制层用例

java 复制代码
/**
 * 计算器 REST 控制器 - 用于演示 JaCoCo 集成测试覆盖率
 */
@RestController
@RequestMapping("/api/calculator")
@RequiredArgsConstructor
public class CalculatorController {

    private final CalculatorService calculatorService;

    /**
     * 加法
     */
    @GetMapping("/add")
    public Map<String, Object> add(@RequestParam int a, @RequestParam int b) {
        int result = calculatorService.add(a, b);
        return Map.of("operation", "add", "a", a, "b", b, "result", result);
    }

    /**
     * 减法
     */
    @GetMapping("/subtract")
    public Map<String, Object> subtract(@RequestParam int a, @RequestParam int b) {
        int result = calculatorService.subtract(a, b);
        return Map.of("operation", "subtract", "a", a, "b", b, "result", result);
    }

    /**
     * 除法
     */
    @GetMapping("/divide")
    public Map<String, Object> divide(@RequestParam int a, @RequestParam int b) {
        double result = calculatorService.divide(a, b);
        return Map.of("operation", "divide", "a", a, "b", b, "result", result);
    }

    /**
     * 全局异常处理 - 捕获 IllegalArgumentException
     */
    @ExceptionHandler(IllegalArgumentException.class)
    public Map<String, Object> handleIllegalArgument(IllegalArgumentException e) {
        return Map.of("error", true, "message", e.getMessage());
    }
}

服务层用例

java 复制代码
/**
 * 计算器服务 - 用于演示 JaCoCo 代码覆盖率
 */
@Service
public class CalculatorService {

    /**
     * 加法
     */
    public int add(int a, int b) {
        return a + b;
    }

    /**
     * 减法
     */
    public int subtract(int a, int b) {
        return a - b;
    }

    /**
     * 除法(包含异常分支)
     */
    public double divide(int a, int b) {
        if (b == 0) {
            throw new IllegalArgumentException("除数不能为零");
        }
        return (double) a / b;
    }

}

这些用例就是我们日常开发的业务。

3.3 测试用例

测试用例可以使用Spring容器也可以不使用。使用Spring容器调用比较简单,但是编译的时候使用的时间可能比较长。

测试用例需要通过Mock的方式调用,此处使用Spring容器的方式调用。

java 复制代码
@SpringBootTest
@AutoConfigureMockMvc
@DisplayName("Calculator 集成测试 - Spring 容器模式")
class CalculatorIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    // ===================== 基本运算 API =====================

    @Nested
    @DisplayName("基本运算 API 测试")
    class BasicApi {

        @Test
        @DisplayName("加法 API -> /api/calculator/add")
        void testAdd() throws Exception {
            mockMvc.perform(get("/api/calculator/add")
                            .param("a", "10")
                            .param("b", "20"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.result", is(30)))
                    .andExpect(jsonPath("$.operation", is("add")));
        }

        @Test
        @DisplayName("减法 API -> /api/calculator/subtract")
        void testSubtract() throws Exception {
            mockMvc.perform(get("/api/calculator/subtract")
                            .param("a", "30")
                            .param("b", "10"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.result", is(20)))
                    .andExpect(jsonPath("$.operation", is("subtract")));
        }

        @Test
        @DisplayName("除法 API -> /api/calculator/divide (正常)")
        void testDivide() throws Exception {
            mockMvc.perform(get("/api/calculator/divide")
                            .param("a", "10")
                            .param("b", "4"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.result", is(2.5)))
                    .andExpect(jsonPath("$.operation", is("divide")));
        }
    }

    // ===================== 全局异常处理器 =====================

    @Nested
    @DisplayName("全局异常处理测试")
    class ExceptionHandler {

        @Test
        @DisplayName("除数为零 -> @ExceptionHandler 捕获 IllegalArgumentException")
        void testDivideByZeroTriggersExceptionHandler() throws Exception {
            mockMvc.perform(get("/api/calculator/divide")
                            .param("a", "10")
                            .param("b", "0"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.error", is(true)))
                    .andExpect(jsonPath("$.message", is("除数不能为零")));
        }
    }
}

测试的时候可以通过类分组,并使用@Nested标识。

3.4 编译执行

bash 复制代码
mvn clean test

运行之后,可以在target看到每个包下面的测试报告,外层为整体的报告。

报告内容:

根据包名可以点击进入。

3.5 覆盖率报告解读

打开 JaCoCo 生成的 HTML 报告,你会看到以下颜色标识:

颜色 含义
🟢 绿色 完全覆盖
🟡 黄色 部分覆盖(如 if 分支只走了一边)
🔴 红色 完全未覆盖
⚪ 无色 无可执行代码(如空行、注释)

报告中的关键指标列

  • Missed Instructions / Total:未覆盖指令数 / 总指令数
  • Missed Branches / Total:未覆盖分支数 / 总分支数
  • Cxty:圈复杂度,值越高表示逻辑越复杂,测试难度越大
  • Missed Lines / Total:未覆盖行数 / 总行数
  • Missed Methods / Total:未覆盖方法数 / 总方法数
  • Missed Classes / Total:未覆盖类数 / 总类数

实战解读技巧

  • 关注红色和黄色的方法,优先补充这些路径的测试用例
  • 圈复杂度高的方法是高风险区域,即使覆盖了也要检查是否所有分支都走到
  • 异常处理分支(catch 块)容易被遗漏,重点检查

04 小结

整体测试下来,如果测试用例编写详细,感觉就没有测试什么事了。但是开发的代码量也就上去了,不知道是不是真的有公司会严格使用呢?

老铁们,你们用没有被要求代码覆盖率需要达到多少才算达标?

相关推荐
渣波1 小时前
基于 Milvus 构建小说知识库 RAG,实现图书智能问答(天龙八部实战)
前端·后端
JavaGuide1 小时前
我最推荐的 4 个 AI 编程 Skills:grill-me、research、diagnosing-bugs、code-review
前端·后端·ai编程
Moment1 小时前
2026 了,前端转 AI 全栈我是这么学的 😍😍😍
前端·后端·面试
唐青枫1 小时前
Java Grails 实战详解:用 Groovy 和 GORM 快速开发 Web 应用
java·groovy
SemiTris1 小时前
Java 异常体系深度解析:从误区到精通
java
荣码2 小时前
AI应用部署上线:Docker打包+API服务+监控告警,我踩了4个坑
java·python
CoderF2 小时前
jstack 如何分析死锁:从线程栈到等待环
后端
xiaotianyuanma2 小时前
【计算机毕业设计】基于java web的社区养老服务管理系统设计与实现
java·开发语言·课程设计
Conan在掘金2 小时前
ArkTS 进阶之道(5):struct 里为啥不能嵌 struct?终态声明 + 状态边界
后端