Java 代码覆盖率(一):从 Emma 到 JaCoCo,覆盖率到底是什么

你写了 200 个测试,全绿。信心满满地说“这个模块测完了”。但问题来了——你的测试跑过多少行代码?哪些 if-else 分支从未走进?哪些方法完全没有测试光顾?覆盖率就是来回答这些问题的。它不会告诉你测试对不对,但会告诉你测试没碰过哪些地方

什么是代码覆盖率

代码覆盖率(Code Coverage)是指测试运行时执行到的代码比例。它可以拆分为多个维度:

维度 含义 举例
行覆盖率(Line) 多少行被执行了 200 / 250 行 = 80%
分支覆盖率(Branch) if/else、switch 的每个分支都走了吗 15 / 20 个分支 = 75%
方法覆盖率(Method) 多少个方法被调了 8 / 10 个方法 = 80%
类覆盖率(Class) 多少个类被触达了 12 / 15 个类 = 80%
圈复杂度覆盖率(Complexity) 每个执行路径都走了吗 配合圈复杂度度量

从视觉上来说,一个覆盖率报告长这样:

Calculator.java - 行覆盖率: 75%
--------------------------------
    public int divide(int a, int b) {
 1      if (b == 0) {                    ← 绿色(测试覆盖了)
 2          throw new ArithmeticException();  ← 红色(没覆盖到!)
        }
 3      return a / b;                    ← 绿色
    }

绿色 = 执行过红色 = 没执行过黄色 = 部分执行(比如一个分支走了,另一个没走)。

代码覆盖率的底层原理:插桩

覆盖率工具怎么知道某行代码有没有被执行?答案叫插桩(Instrumentation)。

在 JVM 编译后的字节码里,覆盖率工具额外插入计数指令——相当于在每个关键位置埋一个计数器:

原始字节码:
    iconst_0          // b == 0 ?
    if_icmpne 20      // 不等,跳到 return a / b
    new #2            // 新建 ArithmeticException
    ...

插桩后:
    iconst_0
    invokestatic CoverageCounter.hit(12)  ← 插入:记录"第 12 行已执行"
    if_icmpne 20
    invokestatic CoverageCounter.hit(13)  ← 插入:记录"第 13 行已执行"
    new #2
    ...

测试跑完后,所有计数器的数据汇总,就能算出哪些行、哪些分支被执行过。

插桩有两种时机

方式 时机 代表工具
On-the-fly(运行时插桩) JVM 加载类时动态植入 JaCoCo 的 agent 模式
Offline(编译期插桩) 编译完成后修改 class 文件 Cobertura、Emma

运行时插桩更常用——不需要修改构建产物,不影响正常编译流程。

覆盖率的历史:Emma → Cobertura → JaCoCo

Java 覆盖率工具的演进,是一部典型的“旧工具消亡、新工具统一”的故事。

Emma(2005 ~ 2012,已停止维护)

Emma 是 Java 覆盖率工具的早期玩家。它的特点是轻量——单个 jar 包就能跑,不需要额外依赖。

# Emma 的使用方式
java -cp emma.jar emma instr -m overwrite -cp target/classes
java -cp emma.jar:target/classes YourTest
java -cp emma.jar emma report -r html -in coverage.em

它的局限也很明显:

  • 不支持 Java 8(2012 年以后就停更了,连 Java 7 的 invokedynamic 支持都不完整)
  • 离线插桩,需要修改 class 文件,流程繁琐
  • 不支持分支覆盖率,只有行覆盖率

所以随着 Java 版本升级,Emma 不可避免地退出舞台。

Cobertura(2006 ~ 2015,已停止维护)

Cobertura 在 Emma 基础上做了提升:

  • 支持分支覆盖率
  • HTML 报告比 Emma 更美观
  • 和 Maven 集成更好(mvn cobertura:cobertura

但 Cobertura 和 Emma 有同样的死穴——离线插桩,且计算方式有问题:Cobertura 默认只统计“被测试触动过的类”,也就是说如果你的类完全没有被测试引用,它不会被算进分母。这导致覆盖率数字虚高——看起来很漂亮,其实大量代码根本没测试。

而且 Cobertura 也停更了,最后一个版本停留在 2015 年。

JaCoCo(2011 ~ 至今,统治级)

JaCoCo(Java Code Coverage)是 Mt. Emma 的同作者开发的新一代工具,名字里就有传承意味。它吸取了 Emma 和 Cobertura 的所有教训,做出了根本性改进:

  Emma Cobertura JaCoCo
插桩方式 离线 离线 运行时(agent) + 离线双模式
行覆盖率
分支覆盖率
指令覆盖率(最精细)
圈复杂度
Java 8+ 支持 ✅(持续更新)
Maven 集成 一般 完美
SonarQube 集成
维护状态 2012 停更 2015 停更 活跃维护

JaCoCo 引入了一个独特的指标——指令覆盖率(Instruction Coverage),这是 Java 字节码层面的覆盖率,比行覆盖更精细:

// 这一行 Java 代码
int result = a > 0 ? a : -a;

// 编译成字节码后可能是多条指令
// iload a
// ifle L1
// iload a        ← 这个分支走到了
// goto L2
// L1: iload a
//    ineg        ← 这个分支没走到?指令覆盖就暴露出来了
// L2: istore result

行覆盖率会说“这一行覆盖了”,但指令覆盖率能告诉你——三元表达式的另一半没走到。

结论:2023 年以后(甚至 2018 年以后),JaCoCo 就是 Java 覆盖率工具的唯一选择。 Emma 和 Cobertura 已成历史,了解它们只是为了理解这个领域的演进脉络。

覆盖率数字的真相

覆盖率是个容易让人产生错觉的指标。

高覆盖率 ≠ 好测试

@Test
void testEverything() {
    service.process(new Request());  // 一行覆盖了 80% 的代码
    // 但:没验证任何返回值
    // 没验证任何副作用
    // 没测试任何异常路径
}

这叫“为了覆盖率而写测试”——行覆盖率 80%,实际验证效果为 0。

低覆盖率 ≠ 坏代码

// 这是自动生成的 equals/hashCode/toString
@Override
public boolean equals(Object o) { /* Lombok 自动生成,20 行 */ }
@Override
public int hashCode() { /* 5 行 */ }
@Override
public String toString() { /* 8 行 */ }

这 33 行拉低了覆盖率,但它们不需要测试——框架生成的代码,框架保证正确性。

100% 覆盖率的成本曲线

覆盖率
100% ┤                              ╱
     ┤                           ╱
 90% ┤                        ╱
     ┤                     ╱
 80% ┤                  ╱
     ┤               ╱
 60% ┤        ╱╲╱╲╱╲╱╲
     ┤──╲╱──
     └──────────────────────────────→ 测试编写时间

从 0% 到 80% 相对容易,从 80% 到 90% 需要精心设计的测试用例,从 90% 到 100% 的成本可能比前面加起来还大——而且收益递减。

务实的目标:核心业务逻辑 80%~90%,工具类/配置类不必强求。

覆盖率在 CI/CD 流水线中的位置

覆盖率处在单元测试和 SonarQube 之间:

代码提交 → 静态检查 → 编译 → 单元测试 + 覆盖率采集 → SonarQube 质量门禁 → 打包
                                    ↑
                              JaCoCo 在此处工作

每次 mvn test,JaCoCo 自动插桩、自动采集、自动生成报告。然后 SonarQube 读取报告,用质量门禁判定:

if 新增代码覆盖率 < 80% → 构建失败,打回修改
if 新增代码覆盖率 ≥ 80% → 放行

小结

代码覆盖率是一个度量工具,不是质量目标

  • 它告诉你测试的盲区在哪——哪些代码从未被测试触碰
  • 不能告诉你测试是否写对了——断言是否正确、边界是否考虑
  • 覆盖率低一定有问题,覆盖率高不说明没问题

下一篇我们会深入 JaCoCo 的实战——如何配置、如何生成报告、如何解读报告里的每个数字、如何把报告喂给 SonarQube。

每天前进一小步,就是一个新的高度!