Skip to content

JaCoCo:覆盖率指标与报告 ​

T1-004必学进阶约 60 分钟JDK 21测试专项 A:单元测试 · 覆盖率与门禁

1. 本节定位 ​

从 《JUnit 5:测试结构与断言》 到 《Mockito:Mock、Stub 与 Verify》 解决了"测试怎么写"。本节回答一个更难的问题:我怎么知道还有哪些代码没被测试碰过?

覆盖率工具就是答案。它不会告诉你"测试写得好不好"(那是 T1-012 的事),但它能精确指出"这块代码一行都没执行过"——这是人工审查很难做到的事。

2. 学习目标 ​

完成本节后,你应该能够:

  • 说出 JaCoCo 的五个覆盖率计数器及其含义;
  • 在 Maven 里配置 JaCoCo 并找到报告(HTML / CSV / XML);
  • 读懂报告里的三类信息:总体数字、类级明细、未覆盖的具体行;
  • 用报告指导补测试(找缺口 → 判断缺口是否重要 → 补测试 → 验证数字变化);
  • 说出覆盖率的三个"不能回答"的问题;
  • 知道 jacoco.exec 的追加模式会怎样误导你。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

看一个真实的问题:你能说出你项目里有哪些方法从来没被测试调用过吗?

靠人工审查很难——测试文件和被测代码的对应关系不是一眼可见的。而覆盖率工具做得又快又准。

但覆盖率也是最容易被误用的指标:一旦它变成"必须达到 90%"的考核项,团队就会开始写没有断言的测试来凑数(T1-013 会展开)。

本节的定位:先学会用它发现缺口,再学会不迷信它。

5. 核心概念 ​

5.1 五个计数器 ​

JaCoCo 从五个维度统计覆盖率:

计数器统计什么特点
指令(INSTRUCTION)字节码指令最细粒度,与源码行不完全对应
分支(BRANCH)if / switch 的每个分支最能反映逻辑覆盖是否充分 ★
行(LINE)源码行最直观,但容易高估(一个 if 行只算一行)
方法(METHOD)方法是否被调用过粗粒度,适合看"整个方法是否被测"
类(CLASS)类是否被加载执行最粗

它们的关系:行覆盖 ≥ 分支覆盖(一行里可能有多个分支)。

为什么分支覆盖最重要:因为它对应"逻辑决策"。一个 if (a && b) 有 4 个分支组合(a真b真、a真b假、a假...短路),只测"a 真 b 真"会让行覆盖满,但分支覆盖不满——而 bug 恰恰常在那些没测到的分支里。

本项目的实测对比(下面 5.4 会展开):

text
行覆盖   73/73 = 100.0%
分支覆盖 33/36 = 91.7%          ← 行满了,分支还有 3 个没走到

5.2 配置 JaCoCo ​

在父 POM 里配置一次,所有模块生效:

xml
<build>
  <plugins>
    <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>
  </plugins>
</build>

两个 execution 的分工:

execution做什么原理
prepare-agent给测试 JVM 挂一个 Java agent运行时探针:在字节码里插入计数逻辑
report读取 .exec 数据,生成报告汇总成 HTML / CSV / XML

注意 prepare-agent 做的是"字节码插桩"——它修改了内存中的类,但不改磁盘上的 class 文件。所以测试跑完,target/classes 里的字节码是干净的。

5.3 报告在哪、怎么看 ​

text
labs/testing-unit/target/site/jacoco/
├── index.html          # 总览(按包分组,可点进去)
├── jacoco.csv          # 明细数据(每类一行,适合脚本处理)
├── jacoco.xml          # 结构化数据(给 CI 工具解析)
└── cn.kb.learn.testing/
    └── *.html          # 每个类的行级详情(红色=未覆盖)
格式用途
HTML人工阅读:从总览点进类,能看到每一行的覆盖情况
CSV脚本处理:一行一个类,方便做门禁(T1-018)
XMLCI 集成:SonarQube、Jenkins 等工具解析它

读 HTML 报告的技巧:

颜色含义
绿色该行被完整覆盖
黄色行被执行了,但分支没走全(重点看这里)
红色该行从未被执行

黄色比红色更值得关注——红色通常是"这个功能还没测",一眼可见;黄色是"看起来测了,其实有分支没走到",隐蔽得多。

5.4 本项目的真实数据 ​

第一次采集(86 个测试):

text
类数 8
指令覆盖 282/323 = 87.3%
分支覆盖 31/36  = 86.1%
行覆盖   66/73  = 90.4%

User            行 2/2   分支 0/0
UserService     行 13/18 分支 5/6      ← 有未覆盖
Payment         行 1/1   分支 0/0
OrderService    行 14/14 分支 2/2
Article         行 1/1   分支 0/0
OrderCalculator 行 17/19 分支 12/14    ← 有未覆盖
PasswordValidator 行 6/6 分支 4/4
ArticleService  行 12/12 分支 8/10     ← 有未覆盖

报告直接告诉我们三件事:

  1. UserService 行覆盖 13/18:有 5 行从未执行——因为前面的测试只测了 rename,delete 方法一次都没被调用过;
  2. OrderCalculator 分支 12/14:三参数版本的两处参数校验没走到;
  3. ArticleService 分支 8/10:有分支缺口。

照着报告补测试后(88 个测试):

text
指令覆盖 311/323 = 96.3%
分支覆盖 33/36  = 91.7%
行覆盖   73/73  = 100.0%          ← 行满了

注意这个对比:行覆盖已经 100%,但分支覆盖只有 91.7%。

这就是"用覆盖率指导补测试"的完整闭环:

text
跑测试 → 看报告 → 找到未覆盖的类/分支 → 判断"这个缺口重要吗"
      → 补测试 → 再跑一次 → 数字上升

补测试的代码(CoverageGapTest):

java
@Test
@DisplayName("UserService.delete:此前完全未被覆盖的方法")
void covers_delete_method() {
    UserRepository repository = mock(UserRepository.class);
    Notifier notifier = mock(Notifier.class);
    when(repository.findById("u1"))
            .thenReturn(Optional.of(new User("u1", "张三", "zhangsan@test.com")));
    UserService service = new UserService(repository, notifier);

    service.delete("u1");

    verify(repository).delete(new User("u1", "张三", "zhangsan@test.com"));
    verify(notifier).send("u1", "账号已删除");
}

效果:UserService 的行覆盖从 13/18 升到 18/18。

关键点:这不是"为了覆盖率写测试",而是"覆盖率指出了真实的功能缺口"——delete 方法确实从来没有被验证过,这是一个应该被发现的漏洞。

5.5 覆盖率不能回答的三个问题 ​

覆盖率说不了的事例子
断言是否有效一个测试调用方法但不断言,仍然贡献 100% 覆盖
逻辑是否正确测试和实现有同样的错误时,覆盖率高且全绿
是否覆盖了该测的场景分支走了,但输入数据不是边界值(如只测了 n=5 没测 n=0)

尤其是第一条,它直接引出了覆盖率最大的陷阱(见 T1-013):

java
@Test
void covers_everything_without_asserting() {
    service.rename("u1", "新名");       // 只调用,不断言
    // 覆盖率 +100%,保护力 +0
}

结论:覆盖率是"发现缺口"的工具,不是"证明质量"的指标。

5.6 三个使用建议 ​

  1. 关注趋势而非绝对值:从 60% 涨到 75% 是进步;停在 91.7% 不是失败;
  2. 重点看"新增代码"的覆盖率:老代码的覆盖率很难补,新代码没测试才是问题(T1-018 的"只降不升"策略);
  3. 每次看报告都要问"未覆盖的那部分重要吗":有些代码(如 catch (Exception) 的兜底分支、main 方法)不值得为覆盖率补测试。

6. 递进示例 ​

6.1 最小示例:跑一次看报告 ​

powershell
cd labs
./mvnw -pl testing-unit test
# 报告生成在 testing-unit/target/site/jacoco/index.html

6.2 业务示例:照着报告补测试 ​

见 5.4 节。三步法:

  1. 看类级明细:找出行覆盖或分支覆盖明显偏低的类;
  2. 进 HTML 看具体行:黄色的行就是"分支没走全"的地方;
  3. 判断 + 补测试:缺口重要就补,不重要就记下来(比如"这个兜底分支只在磁盘满时触发")。

6.3 反例:为了覆盖率而写测试 ​

java
// 反例:调用了一遍,什么也没验证
@Test
void increase_coverage() {
    service.delete("u1");        // 没有断言,只要不抛异常就算"覆盖"了
}

问题:它让覆盖率上升,但没有验证任何行为。如果 delete 里把 repository.delete(user) 写成了 repository.save(user),这个测试仍然绿。

正确写法见 5.4 节的代码——调用之后必须验证"做了什么"。

7. 完整可运行代码 ​

配置(labs/pom.xml,父 POM 对所有模块生效):

xml
<build>
  <plugins>
    <!-- 覆盖率采集与报告:prepare-agent 在测试前挂载探针,report 在 test 阶段生成报告 -->
    <plugin>
      <groupId>org.jacoco</groupId>
      <artifactId>jacoco-maven-plugin</artifactId>
      <!-- 0.8.12 不认识 JDK 25 的内部类字节码,会刷 IllegalClassFormatException 警告 -->
      <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>
  </plugins>
</build>

运行命令:

powershell
cd labs
./mvnw -pl testing-unit test

预期输出(节选):

text
[INFO] Tests run: 88, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- jacoco:0.8.15:report (report) @ testing-unit ---
[INFO] Loading execution data file C:\...\labs\testing-unit\target\jacoco.exec
[INFO] Analyzed bundle 'labs-testing-unit' with 8 classes
[INFO] BUILD SUCCESS

读取报告数字(本项目附带的小脚本能直接算出来):

powershell
cd labs
python -X utf8 -c "import csv; rows=list(csv.DictReader(open('testing-unit/target/site/jacoco/jacoco.csv',encoding='utf-8'))); ti=sum(int(r['INSTRUCTION_COVERED'])+int(r['INSTRUCTION_MISSED']) for r in rows); ci=sum(int(r['INSTRUCTION_COVERED']) for r in rows); tb=sum(int(r['BRANCH_COVERED'])+int(r['BRANCH_MISSED']) for r in rows); cb=sum(int(r['BRANCH_COVERED']) for r in rows); tl=sum(int(r['LINE_COVERED'])+int(r['LINE_MISSED']) for r in rows); cl=sum(int(r['LINE_COVERED']) for r in rows); print(f'指令 {ci}/{ti}={ci/ti*100:.1f}%  分支 {cb}/{tb}={cb/tb*100:.1f}%  行 {cl}/{tl}={cl/tl*100:.1f}%')"
text
指令 311/323=96.3%  分支 33/36=91.7%  行 73/73=100.0%

三个数字要连起来读:

数字说明
行 100.0%每一行代码都执行过
分支 91.7%但有 3 个分支没走到(行满了不等于逻辑测全了)
指令 96.3%有 12 条指令未执行(通常是未走到分支里的那几行)

报告里未覆盖的具体位置:打开 target/site/jacoco/index.html,进入 UserService 与 ArticleService,能看到黄色的行——它们对应 userId == null || userId.isBlank() 这类短路判断中"左边为真"的那一支。

8. Debug 观察点 ​

  1. 打开 target/site/jacoco/index.html,点击 ArticleService,找到黄色的行——观察它标出的分支数(如 2 of 4 branches missed),这比"整行未覆盖"精确得多;
  2. 在 prepare-agent 执行的那一行(Maven 日志里)观察它为测试 JVM 加的 -javaagent 参数——这就是"运行时插桩"的实现方式;
  3. 跑完测试后查看 target/classes/cn/kb/learn/testing/UserService.class 的文件修改时间——它没有因为插桩而改变,证明插桩只发生在内存中;
  4. 用 jacoco.csv 打开一个类,对比 LINE_COVERED 与 BRANCH_COVERED 的比例——感受两者的差距。

9. 常见错误 ​

错误 1:jacoco.exec 的追加模式让覆盖率虚高(本项目实测踩到) ​

现象:删掉了一个测试,重跑后覆盖率没有下降。

原因:JaCoCo 的 prepare-agent 默认 append=true——每次运行把新数据追加到已有的 jacoco.exec。

实测过程:

text
# 当时存在一个临时测试(补 null 分支),分支覆盖 36/36 = 100%
# 删除那个临时测试、重跑测试后:
分支 36/36 = 100%          ← 数据仍然显示 100%,实际应回到 91.7%
# 手动删除 jacoco.exec 后重跑:
分支 33/36 = 91.7%         ← 真实值

为什么危险:本地开发时你会看到一个"比真实情况更好"的数字。CI 上通常没问题(每次都是干净检出),所以会出现"本地 100%、CI 91.7%"的困惑。

修复:删掉 target/jacoco.exec 后重跑;或在配置里显式改:

xml
<configuration>
  <append>false</append>       <!-- 每次重新采集 -->
</configuration>

建议:本地怀疑数字时先删 target 目录重跑;CI 上保持默认(干净环境无影响)。

错误 2:新 JDK 下 JaCoCo 刷大量异常(本项目实测踩到) ​

text
java.lang.instrument.IllegalClassFormatException: Error while instrumenting
sun/util/resources/cldr/provider/CLDRLocaleDataMetaInfo with JaCoCo 0.8.12
  Caused by: java.io.IOException: Error while instrumenting ...

现象:构建仍然成功,但输出被大量 stack trace 淹没。

原因:JaCoCo 会尝试 instrument JDK 内部类,而 0.8.12 不认识 JDK 25 的字节码版本。

修复:升级 JaCoCo。

xml
<version>0.8.15</version>      <!-- 0.8.12 → 0.8.15 后警告消失 -->

这条与 T1-010 的 byte-buddy 问题是同一类问题:新 JDK 常常需要同步升级字节码工具链(JaCoCo、Byte Buddy、Lombok、ASM 都属于这一类)。

错误 3:只看行覆盖率 ​

text
行覆盖率 100% → 以为测全了

实际可能:if (a && b) 的四个组合只测了一个,但这一行算"已覆盖"。

修复:同时看分支覆盖率,并以分支为核心判断逻辑覆盖是否充分(T1-013)。

错误 4:把覆盖率当考核指标 ​

一旦覆盖率与绩效挂钩,团队的最优策略就变成"用最少的工作量把数字推上去"——写没有断言的测试、给 getter/setter 补测试、把不可达代码加 @Generated 排除。

修复:覆盖率作为发现问题的手段,不作为目标。门禁只对"新代码"设要求(T1-018)。

错误 5:报告没生成就以为配置错了 ​

text
mvnw test 之后找不到 target/site/jacoco/

检查顺序:① 插件是否加在了当前模块或父 POM 的 <build><plugins> 里(不是 <pluginManagement>——那里只声明不执行);② report execution 的 phase 是否为 test;③ 测试是否真的跑了(Tests run: 0 时报告为空)。

10. 动手实验 ​

目标:完整走一遍"覆盖率指导补测试"的闭环。

步骤:

  1. 跑 ./mvnw -pl testing-unit test,用第 7 节的命令读出三个数字,记录它们;
  2. 删掉 target/jacoco.exec 再跑一次,确认数字没有变化(排除追加模式的干扰);
  3. 打开 HTML 报告,找出所有黄色的行(分支未走全),列出它们的业务含义;
  4. 选其中 2 个缺口补测试(参考 CoverageGapTest 的写法),要求有断言;
  5. 重跑并记录新数字,算出提升幅度;
  6. 故意写一个"只调用不断言"的测试,观察覆盖率上升但保护力为零——说明为什么覆盖率不能证明质量。

验收方式:能说出第 3 步找到的缺口在哪,以及第 6 步为什么危险。

扩展任务:给 labs 的另一个模块(如 collections)也配置覆盖率采集,看看那些"只写了示例、没写测试"的模块覆盖率是多少——这能帮你判断哪些模块值得补测试。

11. 练习题 ​

基础题

  1. 判断:行覆盖率 100% 说明逻辑分支都测到了。(对 / 错)
  2. 填空:JaCoCo 的两个核心 execution 是 ______(挂载探针)与 ______(生成报告)。
  3. 简答:为什么"只调用不断言"的测试能提升覆盖率,却毫无价值?

代码阅读题

text
类名              行覆盖      分支覆盖
OrderService     14/14       2/2
UserService      18/18       5/6
ArticleService   12/12       8/10
  1. 三个类的行覆盖都是 100%,哪个类最需要补测试?为什么?
  2. UserService 的分支缺口最可能在哪里?(提示:读一下它的参数校验)

编程题

  1. 给一个"折扣计算"类建立覆盖率基线:
    • 被测类 Discount:long apply(long amount, String level);level 为 NONE / SILVER / GOLD / 其他(抛异常);
    • 规则:SILVER 减 5%、GOLD 减 10%、金额为负抛异常;
    • 先只写 2 个测试(覆盖 NONE 与 GOLD),跑覆盖率,记录数字;
    • 照着报告补齐剩余分支(SILVER、未知 level、负数金额),每次只补一个测试并记录数字变化;
    • 最后说明:哪一次补测试对覆盖率的提升最大?

综合任务

  1. 为一个既有模块做"覆盖率体检报告":
    • 挑选一个不少于 5 个类的模块(可以用 labs 里的模块,或你自己的项目);
    • 跑出覆盖率,按类排序列出最需要补测试的 3 个类;
    • 对每个类,进入 HTML 报告找出未覆盖的具体行,判断它是"功能没测"还是"防御性分支";
    • 给出补测试的优先级建议(哪些必须补、哪些可以不补);
    • 额外要求:说明你的判断依据,以及"如果只能补一个测试,你会补哪个"。
分级提示

提示 1(方向):第 4 题的关键是"行覆盖相同但分支覆盖不同"——分支缺口才代表逻辑没测全。

提示 2(关键点):第 5 题的 UserService.rename 里有 if (newName == null || newName.isBlank()),只测了 isBlank() 为真的情况。

提示 3(接近答案):第 6 题的"提升最大"通常是第一次补测试——因为从 0 到 1 的边际收益最高(后续补的往往是原本已被间接覆盖的分支)。

12. 参考答案 ​

基础题
  1. 错。行覆盖只统计"这一行有没有被执行",而 if (a && b) 这类判断在一行里包含多个分支。本项目实测出现过"行 100% 但分支 91.7%"的情况。
  2. prepare-agent;report。
  3. 因为它只让代码"被执行",没有验证"执行得对不对"。覆盖率的定义就是"这段代码被碰过没有"——它从一开始就不衡量"行为是否正确"。要证明正确性,必须有断言;而如果断言写错了,覆盖率一点都帮不上忙(T1-005 的"假绿")。
代码阅读题(第 4、5 题)

第 4 题:ArticleService(分支 8/10,缺 2 个)最需要补。

理由:

类行覆盖分支覆盖判断
OrderService100%2/2 满逻辑已被完整覆盖 ✓
UserService100%5/6(缺 1)有一个分支组合没走到
ArticleService100%8/10(缺 2)缺口最大

关键原则:行覆盖相同时,看分支覆盖——分支没走全意味着某条逻辑路径从未被验证过。

第 5 题:最可能在参数校验的短路判断上。

java
if (newName == null || newName.isBlank()) {          // 两个分支
    throw new IllegalArgumentException("新名字不能为空");
}

现有测试传的是 " "(空白字符串),所以走的是"左边为假 → 判右边 → 为真"的路径。"左边为真"(即 newName == null)这一支从未被走到——这就是那 1 个未覆盖的分支。

补测试:

java
@Test
@DisplayName("传 null 时也抛异常(此前未覆盖的分支)")
void rejects_null_name() {
    assertThatThrownBy(() -> service.rename("u1", null))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessage("新名字不能为空");
}

顺带一个设计讨论:这个测试也验证了一个真实的行为契约——"传 null 不会抛 NPE,而是抛带明确消息的 IllegalArgumentException"。这正是分支覆盖比行覆盖更有价值的原因:它逼你思考"每种输入组合会发生什么"。

编程题(第 6 题)
java
package cn.kb.learn.testing;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

@DisplayName("折扣计算:按覆盖率逐步补测试")
class DiscountCoverageTest {
    private static final Map<String, Integer> DISCOUNTS = Map.of(
            "NONE", 0, "SILVER", 5, "GOLD", 10);

    static long apply(long amountInCents, String level) {
        if (amountInCents < 0) {
            throw new IllegalArgumentException("金额不能为负");
        }
        Integer percent = DISCOUNTS.get(level);
        if (percent == null) {
            throw new IllegalArgumentException("未知会员等级:" + level);
        }
        return amountInCents * (100 - percent) / 100;
    }

    @Test
    @DisplayName("无折扣")
    void none() {
        assertThat(apply(10_000L, "NONE")).isEqualTo(10_000L);
    }

    @Test
    @DisplayName("黄金会员 9 折")
    void gold() {
        assertThat(apply(10_000L, "GOLD")).isEqualTo(9_000L);
    }
}

逐步补测试的预期数字(用 jacoco.csv 的 DiscountCoverageTest 所在包统计):

阶段测试数行覆盖分支覆盖说明
起步2部分低只覆盖了 NONE 与 GOLD
补 SILVER3上升上升覆盖了 percent != null 的另一条路径
补未知等级4近乎满上升覆盖 percent == null 分支
补负数金额5满满覆盖最前面的校验分支

哪一次提升最大:通常是第一次。

原因:边际收益递减。第一个测试能覆盖大段"公共代码"(如方法入口、正常路径),后续测试往往只补一两个分支。本项目从 86 个测试到 88 个测试,只增加了 2 个测试就把行覆盖从 90.4% 提到 100%——这就是"用报告精准打击"的效率:

text
86 个测试 → 行 90.4% / 分支 86.1%
88 个测试 → 行 100.0% / 分支 91.7%     ← 只加了 2 个测试

如果没有覆盖率报告,你要靠人工审查才能发现"delete 方法从没被测过"——而这 2 个测试是照着报告找出来的,不是随便写的。

综合任务(第 7 题)

体检报告模板(以本项目为例):

text
【模块】labs/testing-unit(8 个类)
【总体】指令 96.3% / 分支 91.7% / 行 100%

【需要补测试的类(按优先级)】

1. ArticleService(分支 8/10)
   - 缺口位置:title == null || title.isBlank() 与 content == null || content.isBlank() 的短路分支
   - 性质:**功能没测**(参数校验的第一种输入没覆盖)
   - 建议:必须补 —— 传 null 时的行为是明确的契约

2. UserService(分支 5/6)
   - 缺口位置:newName == null 的短路分支
   - 性质:**功能没测**
   - 建议:必须补

3. User(分支 0/0,行 2/2)
   - 缺口位置:无
   - 性质:record 的自动方法,已 100%
   - 建议:不用动

【可以不补的缺口】
- 如果存在 catch (Exception) 的兜底分支:只在极端环境触发,补测试需要大量构造
- main 方法、纯 getter/setter:价值低

【如果只能补一个测试】
补 ArticleService 的 null 标题测试 —— 因为:
① 缺口最大(2 个分支);
② 传 null 是真实会发生的调用错误;
③ 它能固化"抛 IllegalArgumentException 而不是 NPE"这个契约

判断依据(这是本任务的核心):

缺口类型判断处理
业务规则分支(等级判断、状态流转)重要必须补
参数校验的短路分支(null vs 空串)重要建议补(契约明确)
异常兜底分支(catch (Exception))中等视成本决定
框架要求但业务不需要的代码(无参构造、getter)低不补,或用 @Generated 排除

"如果只能补一个"的答案通常是"缺口最多 + 业务最可能发生"的那个——而不是"最容易写的那个"。

最后提醒:不要把这个报告变成"必须补全"的清单。有些缺口是合理的(比如"磁盘满"这种分支),覆盖率的价值在于让你"知道缺口在哪",而不是"消灭所有缺口"。

13. 掌握标准 ​

  • [ ] 能说出五个计数器,并知道"分支覆盖最重要";
  • [ ] 能配置 JaCoCo 并找到 HTML / CSV / XML 三种报告;
  • [ ] 能从报告定位到具体的未覆盖行,并判断它的重要性;
  • [ ] 完整走过一次"看报告 → 补测试 → 数字上升"的闭环;
  • [ ] 知道覆盖率不能证明"断言有效""逻辑正确""场景充分";
  • [ ] 知道 jacoco.exec 的追加模式会误导本地判断。

14. 扩展资源 ​

15. 下一步学习 ​

《分支覆盖率与覆盖率误区》:深入那个"行 100% 但分支 91.7%"的缺口。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引