一、代码覆盖率是什么
代码覆盖率(Code Coverage)不是玄学,它是一个非常朴素的统计指标。
打一个生活化的比方:你请了一个私家侦探去跟踪某人,侦探回来交了一份报告,告诉你他跟踪到了哪些地方、哪些地方没跟到、哪些地方只跟了一半。代码覆盖率工具就是你的"侦探"------它会在测试运行时记录每一行代码有没有被执行到。

代码覆盖率:衡量测试用例执行了多少被测代码的指标,通常按行、分支、方法等维度统计。你可以理解为"测试用例的覆盖面地图"。
在我接触过的项目里,代码覆盖率主要看四个维度:
| 指标 | 含义 | 举例 |
|---|---|---|
| 行覆盖率 | 有多少行代码被执行过 | 一个 100 行的方法,测试跑了 80 行 → 行覆盖率 80% |
| 分支覆盖率 | if/else、switch 的每个分支是否都被走过 | if (x > 0) 只测了 x=1 的情况 → 分支覆盖率 50% |
| 方法覆盖率 | 有多少方法被调用过 | 10 个 public 方法只测了 7 个 → 70% |
| 类覆盖率 | 有多少类被测试触及 | 通常与项目结构有关,不一定每个类都要直接测 |
为什么需要代码覆盖率
说真的,没有覆盖率数据的时候,你对测试质量的判断基本靠"感觉"。有了覆盖率报告之后,这些事就变得清晰了:
- 一眼看出哪些代码完全没被测试碰过------这些就是潜在的 bug 温床
- 发现单测的"假覆盖"------测试跑过了某个方法,但只走了 happy path,异常分支从没触发过
- 给重构提供安全网------覆盖率达标了,你才敢放心改代码
可能有人会问:代码覆盖率 100% 是不是就万事大吉了?
不是。100% 覆盖率只能说明"每行代码都被执行过了",不能说明"所有边界条件都验证了"。比如一个除法运算
a / b,你传了 b=1,这行被覆盖了,但 b=0 的异常分支你没测,代码依然会炸。覆盖率是必要但不充分的质量指标。
二、JaCoCo 是什么
JaCoCo:Eclipse 基金会旗下的开源代码覆盖率工具,通过字节码插桩技术在 JVM 层面收集执行数据。它是目前 Java 生态中覆盖率工具的事实标准,没有之一。
官网在 www.jacoco.org/jacoco/(搜:JaCoCo 官网)。2009 年从 EclEmma 项目发展而来,到现在已经迭代了十几年,Spring Boot、Maven、Gradle 对它都有原生级别的支持。

字节码插桩 :在不修改源码的前提下,往编译后的 .class 文件里插入监控代码的技术。你可以理解为"给代码装上隐形的计数器"。
JaCoCo 的核心原理就是字节码插桩。它不碰你的 Java 源码,而是在编译之后的 .class 字节码中插入探针。运行时这些探针自动记录哪些指令被执行过,最终收集成一份 jacoco.exec 数据文件,再渲染成可读的 HTML 报告。
和早期的 Cobertura、Emma 相比,JaCoCo 有三个碾压级优势:
- 不需要改源码------字节码级插桩,零侵入
- 支持 on-the-fly 插桩------不用提前对 jar 包做处理,运行时直接挂载
- 报告可读性好------HTML 报告里绿/红/黄三色标记,一眼看出哪行漏了
三、Maven 插件配置:3 步接入 Spring Boot
JaCoCo 最方便的接入方式就是 Maven 插件,整个配置不超过 20 行的 pom.xml。
当然,JaCoCo 也支持本地命令行独立使用(去官网下 jar 包,java -javaagent:jacocoagent.jar 手动挂载),但 Maven 插件在构建流程里直接嵌入了覆盖率收集和报告生成,对团队协作更友好。本文选 Maven 插件方案。

在 pom.xml 的 <build><plugins> 中加入以下配置(以 JaCoCo 0.8.15 为例,2026 年 6 月最新稳定版):
xml
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.15</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>

这两个 execution 各司其职:
- prepare-agent:在 Maven Surefire 运行测试之前,把 JaCoCo 的探针 Agent 挂载到 JVM。保证跑测试的时候覆盖率数据已经被悄悄记录了。
- report :在
test阶段(测试跑完之后)读取探针收集的jacoco.exec,生成 HTML / XML / CSV 三种格式的报告。
配置完这一步,JaCoCo 就接入完成了。接下来写代码验证。
四、示例代码:一个简单的 User CRUD
我这边用一个 Spring Boot 项目来演示,代码尽量精简,只保留能体现"覆盖率差异"的关键逻辑。
User 实体------标准的 POJO,不需要测试:
java
// src/main/java/.../model/User.java
public class User {
private Long id;
private String name;
private String email;
public User() {}
public User(Long id, String name, String email) {
this.id = id; this.name = name; this.email = email;
}
// getter / setter 省略
}
UserService ------核心业务逻辑,包含增删改查。注意 findByName 用了 equalsIgnoreCase,delete 返回 boolean 而非抛异常------这些细节在覆盖率报告里都会体现:
java
// 示例:src/main/java/.../service/UserService.java
@Service
public class UserService {
private final ConcurrentHashMap<Long, User> store = new ConcurrentHashMap<>();
private final AtomicLong idGen = new AtomicLong(1);
public List<User> findAll() {
return new ArrayList<>(store.values());
}
public Optional<User> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
public User create(User user) {
user.setId(idGen.getAndIncrement());
store.put(user.getId(), user);
return user;
}
public Optional<User> update(Long id, User updated) {
return findById(id).map(existing -> {
existing.setName(updated.getName());
existing.setEmail(updated.getEmail());
return existing;
});
}
public boolean delete(Long id) {
return store.remove(id) != null;
}
public List<User> findByName(String name) {
return store.values().stream()
.filter(u -> u.getName().equalsIgnoreCase(name))
.toList();
}
}
五、写单元测试:覆盖不是"跑通就行"
测试代码决定了覆盖率的上限。我们用一个常见的分层写法------Service 层测逻辑,Controller 层用 @WebMvcTest + Mock 测路由。
UserServiceTest------每个方法至少测两条路径:正常情况 + 边界/异常情况:
java
// 示例:src/test/java/.../service/UserServiceTest.java
class UserServiceTest {
private UserService userService;
@BeforeEach
void setUp() {
userService = new UserService();
}
@Test
@DisplayName("findById should return user when exists")
void shouldFindUser() {
User created = userService.create(
new User(null, "Alice", "alice@example.com"));
Optional<User> found = userService.findById(created.getId());
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("Alice");
}
@Test
@DisplayName("findById should return empty for non-existent id")
void shouldReturnEmpty() {
assertThat(userService.findById(999L)).isEmpty();
}
@Test
@DisplayName("update should return empty when user not found")
void shouldReturnEmptyOnUpdate() {
Optional<User> result = userService.update(
999L, new User(null, "Ghost", "ghost@example.com"));
assertThat(result).isEmpty();
}
@Test
@DisplayName("delete should return false when user not found")
void shouldReturnFalseOnDelete() {
assertThat(userService.delete(999L)).isFalse();
}
@Test
@DisplayName("findByName should be case-insensitive")
void shouldFindByNameCaseInsensitive() {
userService.create(new User(null, "alice", "a@example.com"));
userService.create(new User(null, "ALICE", "a2@example.com"));
List<User> found = userService.findByName("alice");
assertThat(found).hasSize(2);
}
}
这里强调一点:别只测 happy path 。update 和 delete 的 "不存在" 分支如果不写测试,JaCoCo 的分支覆盖率会直接告诉你只走了一半。我见过不少团队覆盖率卡在 40% 左右上不去,原因就是只测了正向逻辑。
六、运行并看懂 JaCoCo 报告
配置好了,代码和测试也写好了,跑一下:
bash
mvn clean test

测试跑完后,JaCoCo 报告生成在:
bash
target/site/jacoco/index.html
用浏览器打开这个文件,你会看到一个彩色仪表盘。

报告怎么看?关键看这 5 列
JaCoCo 默认用五种颜色区分覆盖程度:
| 颜色 | 含义 |
|---|---|
| 🟢 绿色 | 该行/分支被完整执行 |
| 🟡 黄色 | 部分覆盖(如 if 只走了 true 分支) |
| 🔴 红色 | 完全未被覆盖,盲区 |
| ⚪ 无色 | 不可执行代码(方法签名、空行) |
点进某个具体的类(比如 UserService),会看到逐行的源码着色:

以我这边项目的 UserService 为例,如果所有测试都写了(包含空值分支和异常路径),行覆盖率能到 100%,分支覆盖率也能到 100%。但如果少写了 delete 的 return false 分支测试,分支覆盖率就会掉到 80% 多。JaCoCo 不会骗人,一目了然。
行覆盖率 vs 分支覆盖率:哪个更重要
| 维度 | 行覆盖率 | 分支覆盖率 |
|---|---|---|
| 计算方式 | 执行过的行数 / 总可执行行数 | 执行过的分支数 / 总分支数 |
| 容易虚高吗 | 容易------一行 if 走了 true 就计入 | 不容易------两个分支都得走才算 |
| 能暴露什么问题 | 哪段逻辑完全没跑过 | 哪些条件没验证过 |
| 推荐关注度 | ★★★ | ★★★★★ |
我的建议:把分支覆盖率当作主要参考指标。 一行代码被覆盖只能说明"来过",一个分支被覆盖才能说明"所有可能性都考虑到了"。很多 bug 就藏在那个你没有测到的 else 分支里。
七、常见问题
可能有人会问:
mvn test跑完了,但target/site/jacoco/目录不存在,报告没生成?检查两件事:第一,
prepare-agent和report两个 execution 是否都配了(我见过只配了 agent 没配 report 的情况);第二,确认 Maven Surefire 插件确实执行了测试(看终端有没有Tests run:字样)。如果用了<skipTests>true</skipTests>,那肯定没有报告。
有人会问:多模块 Maven 项目怎么配?在父 pom 的
<pluginManagement>中声明 JaCoCo 插件,子模块不需要重复配置。如果要生成聚合报告,再加一个report-aggregategoal,单独建一个coverage-report模块来汇总。
覆盖率太低怎么办?别一上来就追 80% 的硬指标,那不现实。先让 JaCoCo 把报告跑出来,按类排序看红色最多的几个类,从风险最高的入手。通常一个项目最该优先补测试的是:核心业务 Service、工具类中的复杂算法、Controller 的异常处理分支。POJO、配置类、启动类可以放最后。
总结
JaCoCo 是目前 Java 项目做代码覆盖率检测的首选工具。接入成本极低------pom.xml 加两段 execution,跑 mvn test 就能出 HTML 报告。但工具的价值不在报告本身,在于你看了报告之后做了什么:补了哪些漏掉的测试、发现了哪些假覆盖、重构时有没有底气。
如果你现在手上有一个 Spring Boot 项目,花 10 分钟按本文配一下 JaCoCo 代码覆盖率,跑一份报告出来看看------你可能会有"惊喜"(或者惊吓)。