QA之二 - 单元测试-- JaCoCo
一、是什么
JaCoCo 是 Java 生态最主流的代码覆盖率工具,能统计单元测试对代码的覆盖程度(哪些代码被执行、哪些没被执行),生成可视化报告,帮你发现测试遗漏的分支 / 代码行。
覆盖维度
- 行覆盖(Line):代码行是否被执行(最常用);
- 分支覆盖(Branch):条件分支是否全覆盖(if/else、switch);
- 方法覆盖(Method):方法是否被调用过;
- 类覆盖(Class):类是否有至少一个方法被调用;
二、核心原理
JaCoCo 基于字节码插桩(Instrumentation) 实现,无需修改源代码:
- 测试执行前:JaCoCo 对被测类的字节码插入 “探针”(统计执行次数);
- 测试执行中:探针记录每个代码行 / 分支的执行情况;
- 测试执行后:JaCoCo 收集探针数据,生成 HTML/XML/CSV 格式的覆盖率报告。
1、什么是字节码插桩?
- 官方定义:字节码插桩是在不修改源代码的前提下,对编译后的 Java 字节码(.class 文件)进行动态 / 静态修改,插入额外的代码(“探针”/“钩子”),以实现监控、统计、调试等功能的技术。
- 大白话:把 Java 字节码看作 “半成品程序”,插桩就是在这个半成品里 “偷偷加代码”—— 比如在方法开头加一行 “统计执行次数”,在 if 分支加一行 “记录是否执行”,但源码完全不变。
2、常见使用场景:测试 / 监控工具
- JaCoCo:插桩统计代码行 / 分支的执行次数(覆盖率);
- Mockito:插桩生成 Mock 对象的字节码(模拟外部依赖);
- Arthas:动态插桩监控线上应用的方法调用、参数、返回值;
- APM 工具(如 SkyWalking):插桩收集调用链路、性能数据。
3、插桩分类
根据插桩的执行时机,分为 静态插桩 和 动态插桩,二者各有适用场景:
-
静态插桩(编译后 / 打包前修改字节码)
- 在代码编译完成后、部署运行前,直接修改 .class 文件或 .jar 包中的字节码,生成新的字节码文件。
- 优点:修改一次,多次运行生效;性能无实时损耗;
- 缺点:无法动态调整,需重新打包 / 部署;
- 工具:ASM、Javassist、ByteBuddy(静态模式);
- 典型应用:JaCoCo 离线插桩、代码混淆工具。
-
动态插桩(运行时修改字节码)
- 在 JVM 加载类的过程中(类加载的 transform 阶段),动态修改字节码,无需修改物理 .class 文件。
- 优点:无需重新打包 / 部署,运行时动态生效;可按需开启 / 关闭;
- 缺点:类加载时插桩有轻微性能损耗;无法修改已加载的类(除非重加载);
- 工具:Java Agent、ByteBuddy(动态模式)、Arthas、BTrace;
- 典型应用:JaCoCo 运行时插桩、Mockito 动态生成 Mock 对象、Arthas 线上监控。
核心工具对比(JaCoCo 用 ASM,Mockito 用 ByteBuddy)
- JaCoCo:底层用 ASM 实现静态 / 动态插桩,插入 “探针” 统计代码执行次数;
- Mockito:用 ByteBuddy 动态生成 Mock 对象的字节码(无需手动创建类);
- Arthas:基于 Java Agent + ASM 实现线上动态插桩,监控方法调用。
三、怎么用?
1、在 pom.xml 中配置 JaCoCo 插件
点击查看代码
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>Test2</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<!-- 统一指定项目编码为 UTF-8 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 统一指定项目报告编码(可选,避免报告乱码) -->
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!-- JUnit 5 核心依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<!-- 参数化测试依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<!-- Mockito 核心依赖 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<!-- Mockito 适配 JUnit 5 的扩展 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<!-- 可选:静态方法Mock依赖(如需Mock静态工具类) -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<!-- 执行快速、独立的单元测试(失败会直接中断构建)-->
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<!-- 专门用于执行集成测试(Integration Tests) -->
<!--<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.1.2</version>
</plugin>-->
<!-- 1. JaCoCo 核心插件(生成覆盖率报告) -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version> <!-- 最新稳定版 -->
<configuration>
<excludes>
<!-- 排除配置类 -->
<exclude>com/multisig/config/**/*</exclude>
<!-- 排除工具类常量 -->
<exclude>com/multisig/constants/**/*</exclude>
<!-- 排除测试专用代码 -->
<exclude>**/*Test*.class</exclude>
</excludes>
</configuration>
<executions>
<!-- 执行测试时收集覆盖率数据 -->
<execution>
<id>jacoco-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<!-- 测试完成后生成覆盖率报告 -->
<execution>
<id>jacoco-report</id>
<phase>test</phase> <!-- 绑定到 mvn test 阶段 -->
<goals>
<goal>report</goal>
</goals>
</execution>
<!-- 可选:强制执行覆盖率规则(如行覆盖率≥80%) -->
<execution>
<id>jacoco-check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<!-- 对所有代码的行覆盖率要求 ≥80% -->
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.8</minimum> <!-- 80% 行覆盖 -->
</limit>
<!-- 分支覆盖率 ≥70% -->
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.7</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<!-- 2. 确保 Maven Surefire 插件适配 JUnit 5 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<includes>
<include>**/*Test.java</include> <!-- 执行所有测试类 -->
</includes>
</configuration>
</plugin>
</plugins>
</build>
</project>
2、被测代码
点击查看代码
package com.example;
public class MultisigService {
private RpcClient rpcClient;
private int threshold = 2; // 多签门限
public MultisigService(RpcClient rpcClient) {
this.rpcClient = rpcClient;
}
// 核心方法:执行多签交易(含多个分支/异常)
public boolean executeMultisigTx(String txId, int signatureCount) {
// 分支1:空值校验
if (txId == null || txId.isEmpty()) {
return false;
}
// 分支2:签名数校验
if (signatureCount < threshold) {
return false;
}
// 分支3:RPC调用(含异常)
try {
return rpcClient.sendTx(txId);
} catch (RuntimeException e) {
// 异常分支
return false;
}
}
// 辅助方法:获取门限(未被测试覆盖)
public int getThreshold() {
return this.threshold;
}
}
package com.example;
public interface RpcClient {
boolean sendTx(String txId);
}
3、单元测试代码(JUnit 5 + Mockito)
点击查看代码
package com.example;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class MultisigServiceTest {
@Mock
private RpcClient rpcClient;
@InjectMocks
private MultisigService multisigService;
@Test
@DisplayName("测试txId为空时返回false")
void testEmptyTxId() {
assertFalse(multisigService.executeMultisigTx("", 2));
}
@Test
@DisplayName("测试签名数不足时返回false")
void testInsufficientSignature() {
assertFalse(multisigService.executeMultisigTx("tx-001", 1));
}
@Test
@DisplayName("测试RPC返回成功时返回true")
void testRpcSuccess() {
when(rpcClient.sendTx("tx-002")).thenReturn(true);
assertTrue(multisigService.executeMultisigTx("tx-002", 2));
}
// 补充测试:RPC抛异常分支
@Test
@DisplayName("测试RPC抛异常时返回false")
void testRpcException() {
when(rpcClient.sendTx("tx-003")).thenThrow(new RuntimeException("RPC超时"));
assertFalse(multisigService.executeMultisigTx("tx-003", 2));
}
// 补充测试:getThreshold方法
@Test
@DisplayName("测试获取门限方法")
void testGetThreshold() {
assertEquals(2, multisigService.getThreshold());
}
}
4、执行命令
- mvn clean test:执行单元测试 + 生成 JaCoCo 覆盖率报告
- mvn jacoco:report:单独生成覆盖率报告(无需重新执行测试)
- mvn jacoco:check:单独检查覆盖率是否满足规则
5、报告生成位置
JaCoCo 会在项目目录下生成报告:
点击查看代码
target/
└── site/
└── jacoco/
├── index.html # 核心HTML报告(打开即可可视化查看)
├── jacoco.xml # XML报告(CI/CD用)
└── jacoco.csv # CSV报告(数据分析用)
6、JaCoCo 报告解读
打开 target/site/jacoco/index.html,核心界面分为 3 部分

6.1)概览页(项目级覆盖率)

6.2)类详情页
代码行标注颜色:
✅ 绿色:已覆盖(如 txId.isEmpty() 行);
❌ 红色:未覆盖(如 catch (RuntimeException e) 行、getThreshold() 方法);
⚠️ 黄色:部分覆盖(如 if (signatureCount < threshold) 仅测试了 true 分支?不,本例中测试了 true,false 未测?需看具体)。
6.3)分支详情
if (signatureCount < threshold):true 分支被测试(签名数 = 1),false 分支被测试(签名数 = 2)→ 100% 覆盖;
try/catch:try 块被测试(RPC 成功),catch 块未测试 → 50% 覆盖;
if (txId == null || txId.isEmpty()):true 分支被测试(空 txId),false 分支被测试(非空 txId)→ 100% 覆盖。
7、排除无需覆盖的代码
项目中,部分代码(如配置类、工具类常量)无需统计覆盖率,可在 JaCoCo 中排除。
7.1)JaCoCo 排除的核心场景
代码完全无需统计覆盖率,排除后能让覆盖率指标更精准:
| 排除场景 | 典型示例 |
|---|---|
| 配置类 / 常量类 | MultisigConfig(多签服务配置)、ChainConstants(链上常量) |
| 工具类静态常量 / 空方法 | SignatureUtils 中的常量、RpcUtils 中的空构造方法 |
| 测试专用代码 | TestDataGenerator(测试数据生成)、MockRpcClient |
| 第三方依赖 / 生成代码 / 空方法 | OpenAPI 生成的 API 层代码、自动生成的实体类 |
| 特定方法(如 getter/setter) | MultisigService.getThreshold()(简单取值方法) |
| 单行代码(如日志 / 注释) | 方法内的日志打印行、异常捕获后的兜底行 |
7.2) 如何配置
(1) 包 / 类级排除
点pom.xml
<!-- Maven 配置排除 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<configuration>
<!-- 核心:排除配置 -->
<excludes>
<!-- 1. 排除整个包(通配符:** 匹配任意子包,* 匹配任意类) -->
<exclude>com/multisig/config/**/*</exclude> <!-- 排除配置包 -->
<exclude>com/multisig/constants/**/*</exclude> <!-- 排除常量包 -->
<!-- 2. 排除指定类(精确匹配) -->
<exclude>com/multisig/MockRpcClient.class</exclude> <!-- 排除Mock类 -->
<!-- 3. 排除匹配规则的类(通配符) -->
<exclude>com/multisig/*DTO.class</exclude> <!-- 排除所有DTO实体类 -->
<exclude>com/multisig/*Utils.class</exclude> <!-- 排除所有工具类 -->
<!-- 4. 排除测试生成代码 -->
<exclude>com/multisig/api/**/*</exclude> <!-- 排除OpenAPI生成的API层 -->
</excludes>
</configuration>
<!-- 其余executions配置不变 -->
</plugin>
配置通配符规则
| 通配符 | 含义 | 示例 | 匹配结果 |
|---|---|---|---|
| ** | 匹配任意层级的目录 | com/multisig/**/* | 匹配 com/multisig 下所有类 |
| * | 匹配任意字符(无层级) | com/multisig/*Utils.class | 匹配 com/multisig/RpcUtils.class |
| ? | 匹配单个字符 | com/multisig/M?ck.class | 匹配 com/multisig/Mock.class |
(2) 类 / 方法级排除(注解方式,精准控制)
JaCoCo 支持通过自定义注解标记需要排除的类 / 方法,无需修改插件配置,这种不推荐。
(3) 单行代码排除(精准过滤单行)
针对方法内无需统计的单行代码(如日志、异常兜底),JaCoCo 支持通过特殊注释排除单行:
在需要排除的代码行末尾添加注释:// $COVERAGE-IGNORE$
点击查看代码
public class MultisigService {
public boolean executeMultisigTx(String txId, int signatureCount) {
if (txId == null || txId.isEmpty()) {
return false;
}
try {
return rpcClient.sendTx(txId);
} catch (RuntimeException e) {
// 排除日志行(无需统计覆盖率)
log.error("RPC调用失败", e); // $COVERAGE-IGNORE$
// 排除兜底返回行(简单逻辑,无需测试)
return false; // $COVERAGE-IGNORE$
}
}
}
(4) 报告级排除(仅过滤报告,不影响插桩)
若仅需在覆盖率报告中隐藏某些代码(但仍会插桩统计),可通过报告过滤实现:
点击查看代码
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<id>jacoco-report</id>
<phase>test</phase>
<goals><goal>report</goal></goals>
<configuration>
<!-- 仅报告中排除,插桩仍会执行 -->
<excludes>
<exclude>com/multisig/utils/**/*</exclude>
</excludes>
</configuration>
</execution>
</executions>
</plugin>

浙公网安备 33010602011771号