bigywater.tech
Java · 428

覆盖率指标的误区

代码覆盖率被广泛当作测试质量的指标,但它只回答“哪些代码被执行过”。这篇用同一份代码上的三套测试,量出覆盖率、分支覆盖与变异测试得分的差别:覆盖率最高的测试可以一个 bug 都发现不了。

100% vs 0%
没有断言的测试:分支覆盖 100%,变异测试得分 0%(26 个变异全部存活)
2 / 4
一行 return a > 0 && b > 0 ? 1 : 2; 在只有一个测试 tier(1, 1) 时的分支覆盖(行覆盖 1 / 1)
1.03 倍
JaCoCo agent 对一个 CPU 型循环的减速

三套测试的覆盖率与变异得分

实测 · 本机执行输出
  [NoAssertTest:没有任何断言的测试(只是调用了所有方法)] 通过 1、失败 0
      Pricing:行覆盖 88%(7/8),分支覆盖 100%(18/18),指令覆盖 96%;Dto:行覆盖 100%,方法覆盖 100%
      Pricing 的变异测试:共 26 个变异,被杀死 0 个(得分 0%),存活 26 个,没有覆盖 0 个
  [ThinTest:只有 2 个断言的薄测试] 通过 2、失败 0
      Pricing:行覆盖 75%(6/8),分支覆盖 33%(6/18),指令覆盖 49%;Dto:行覆盖 0%,方法覆盖 0%
      Pricing 的变异测试:共 26 个变异,被杀死 9 个(得分 35%),存活 5 个,没有覆盖 12 个
  [RealTest:为每个分支与边界写了断言的测试] 通过 9、失败 0
      Pricing:行覆盖 88%(7/8),分支覆盖 100%(18/18),指令覆盖 96%;Dto:行覆盖 50%,方法覆盖 18%
      Pricing 的变异测试:共 26 个变异,被杀死 26 个(得分 100%),存活 0 个,没有覆盖 0 个

行覆盖与分支覆盖

实测 · 本机执行输出
  [ThinTest] 方法 tier(a, b):行 覆盖 1 / 遗漏 0;分支 覆盖 2 / 遗漏 2(a > 0 与 b > 0 各 2 个分支,共 4 个)
  [RealTest] 方法 tier(a, b):行 覆盖 1 / 遗漏 0;分支 覆盖 4 / 遗漏 0(a > 0 与 b > 0 各 2 个分支,共 4 个)

变异测试

实测 · 本机执行输出
  [NoAssertTest] SURVIVED 26 个
      SURVIVED:ConditionalsBoundaryMutator 第 4 行, ConditionalsBoundaryMutator 第 5 行, MathMutator 第 3 行, MathMutator 第 4 行, MathMutator 第 5 行, MathMutator 第 6 行 …
  [RealTest] KILLED 26 个

探针开销

实测 · 本机执行输出
  没有 agent 的中位数 547 ms;有 JaCoCo agent(includes=Spin)的中位数 565 ms,慢 1.03 倍

分步演示

覆盖率指标
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12;JaCoCo 0.8.12(agent + cli);PIT 1.17.1 + pitest-junit5-plugin 1.2.1,默认变异算子;JUnit Jupiter 5.9.2 / console-standalone 1.9.2(与 PIT 的 JUnit 5 插件匹配);被测代码是自写的 Pricing / Dto 小例子
    • 没有验证:真实项目的覆盖率与缺陷率的相关性、Maven / Gradle 插件的覆盖率聚合、分支覆盖对 switch / try-finally / Lombok 生成代码的计数、增量覆盖率(diff coverage)、PIT 的更多变异算子(STRONGER / ALL)与耗时、覆盖率阈值门禁的实际效果
    京ICP备2026067502号-1京公网安备11010502063487号