前置知识: 软件工程与测试

白盒测试覆盖度

7 min中级

覆盖率指标的精确语义:语句、分支、条件、判定-条件、条件组合与 MC/DC 的定义、计算反例与工具实现(JaCoCo、Istanbul、V8 coverage、gcov)。

1. 覆盖率到底在度量什么

覆盖率回答的是一个很具体的问题:测试运行时,代码的哪些部分被执行过。 它是「发现没被测过的代码」的探照灯,而不是「代码质量」的分数。

一个容易混淆的点:行覆盖率(line coverage)不等于语句覆盖率(statement coverage)。一行可能包含多条语句(a = 1; b = 2),也可能一条语句跨多行; JaCoCo 这类字节码工具统计的其实是指令/块覆盖,再映射回源码行展示。不同 工具对「覆盖了一条」的定义略有差异,跨工具比较百分比没有意义。

前置知识:分支与条件表达式、控制流图的基本概念。

flowchart TD
    SC["语句覆盖<br/>最弱"] --> BC["判定/分支覆盖"]
    BC --> CC["条件覆盖<br/>不蕴含分支覆盖"]
    BC --> CDC["判定-条件覆盖"]
    CC --> MCC["条件组合覆盖<br/>指数爆炸"]
    CDC --> MCDC["MC/DC<br/>航空等安全关键领域"]
    MCC --> MCDC

2. 从一个例子算清所有指标

全文使用同一个被测函数,请动手算一遍再往下看:

// 根据分数与是否加分计算结果
String grade(int score, boolean bonus) {
    if (score >= 90 && bonus) {   // 判定 D1,含条件 C1: score>=90、C2: bonus
        return "A+";
    }
    if (score >= 60) {            // 判定 D2
        return "及格";
    }
    return "不及格";
}

2.1 语句覆盖

定义:每条语句至少执行一次。

  • 用例 grade(95, true) 即可让三条 return 全部执行吗?不能——它只走到 return "A+"。还需要 grade(70, false) 走到 "及格"、grade(30, false) 走到 "不及格"。
  • 3 个用例达成 100% 语句覆盖。

语义边界:语句覆盖不关心判定结果的方向。短路表达式 if (a || b) 里 让 a 为真,b 根本不会被执行,但语句覆盖照样达标——这是它最弱的原因。

2.2 判定覆盖(分支覆盖)

定义:每个判定的真、假两个结果至少各出现一次。

  • D1:(95, true) 真 + (70, false) 假
  • D2:(70, false) 真 + (30, false) 假
  • 共 3 个用例达成 100% 分支覆盖。

注意「判定」的单位是整个布尔表达式,而不是其中每个子条件。(95, false) 和 (90, true) 对 D1 来说都是「真」,分支覆盖看不出 bonus 到底有没有 单独起作用——这正是需要条件覆盖的原因。

2.3 条件覆盖

定义:判定中每个原子条件的真、假至少各取一次(不管判定本身的结果)。

  • C1(score>=90):(95, false) 真 + (30, false) 假
  • C2(bonus):(30, true) 真 + (30, false) 假

经典反例(条件覆盖不蕴含分支覆盖):用例 (95, false) 与 (30, true) 让 C1、C2 的真假都取到了,但 D1 两次都是假——「真分支」一次都没走过。 所以条件覆盖与分支覆盖互不蕴含,二者合起来才是下一节。

2.4 判定-条件覆盖

定义:判定结果真假各一次 且 每个条件真假各一次,即上面 2.2 与 2.3 的并集。它修复了 2.3 的反例,但仍有一个盲区:无法发现某个条件被另一个 条件掩盖的情况(比如 a || b 中 a 恒为真时,b 写错也测不出来)。

2.5 条件组合覆盖

定义:同一判定内所有条件的真假的全部组合各出现一次。D1 有 2 个 条件,需要 2² = 4 个组合;3 个条件就是 8 个——组合数随条件个数指数增长, 工程上只用于小规模关键逻辑。

2.6 MC/DC(修正条件/判定覆盖)

定义:用最少的用例证明——每个条件都能独立影响判定结果。形式化要求:

  1. 每个判定的真假结果都出现过(分支覆盖);
  2. 判定内每个条件真假各取一次(条件覆盖);
  3. 每个条件存在一个「独立影响对」:仅该条件翻转,判定结果就翻转。

对 n 个条件的判定,MC/DC 只需 n + 1 个用例(条件组合要 2ⁿ 个), 这就是它成为安全关键领域标配的原因:DO-178C(机载软件)对 A 级软件强制 要求 MC/DC,汽车 ASPICE、医疗 IEC 62304 也有类似取向。

用例score >= 90bonusD1说明
T1TTT基准
T2TFF仅翻转 C2 → D1 翻转,C2 独立
T3FTF仅翻转 C1 → D1 翻转,C1 独立

3. 强度对比与选择

指标n 条件用例数蕴含关系适用场景
语句覆盖少最弱最低底线
分支覆盖少蕴含语句覆盖绝大多数业务代码
条件覆盖少与分支覆盖互不蕴含与分支覆盖搭配使用
判定-条件少蕴含分支 + 条件复杂布尔逻辑
MC/DCn + 1蕴含判定-条件(不含条件组合)安全关键领域
条件组合2ⁿ最强小规模关键决策表

工程默认值:分支覆盖起步。业务代码追条件组合覆盖的性价比极低, 把时间花在设计更好的用例上收益更大。

4. 覆盖率工具与实现原理

4.1 三种插桩路线

flowchart LR
    A["源码插桩<br/>Istanbul/nyc"] --> B["运行时收集"]
    C["字节码插桩<br/>JaCoCo"] --> B
    D["引擎原生块覆盖<br/>V8 coverage / gcov"] --> B
  • 源码插桩(JavaScript 的 Istanbul/nyc):重写源码,在每个语句/分支 前插入计数器。能精确知道「分支两个方向各走了几次」,代价是运行变慢、 源码被改写。
  • 字节码插桩(Java 的 JaCoCo):在 class 文件层面插入探针,对使用者 透明,Java 事实标准。注意 JaCoCo 不支持 MC/DC(社区长期讨论中), 安全关键领域的 Java 项目要选商业工具(如 VectorCAST/Jenkins 配合的 TCMS 类方案)。
  • 引擎原生覆盖(V8 coverage、gcc/gcov、llvm-cov):由运行时/编译器 直接输出块覆盖数据,几乎零侵入、速度快。V8 原生给出的是块(block) 覆盖,分支方向等语义信息要靠 AST 重映射还原——Vitest 3.2 起 @vitest/coverage-v8 正是通过这一步把精度追平了 Istanbul,同时保留了 不改源码的速度优势(v8 也是 Vitest 的默认 provider;切换 Istanbul 只需 装 @vitest/coverage-istanbul 并设 coverage.provider)。

4.2 典型配置速览

// vitest.config.ts —— 覆盖率门禁示例
export default {
  test: {
    coverage: {
      provider: 'v8',            // 默认即 v8,可换 istanbul
      include: ['src/**'],
      thresholds: {
        branches: 80,            // 四项指标低于阈值时命令失败
        functions: 80,
        lines: 80,
        statements: 80,
      },
    },
  },
};
# Java:surefire/jacoco 插件执行后生成报告
mvn verify
# 报告位于 target/site/jacoco/index.html

5. 常见陷阱

  • 把 100% 覆盖当目标:覆盖率只说明代码「被执行」,不说明「被验证」。 一个没有断言的测试也能刷高覆盖率——先审计断言质量,再看数字。
  • 断言为空的假覆盖:@Test void t() { service.run(); } 语句覆盖率 100%, 却什么都没证明。评审时重点看异常路径的断言。
  • 难以覆盖的代码是设计信号:必须私有方法、单例、静态初始化才能测的 代码,通常耦合过重。覆盖率卡点反复上不去的模块,优先重构而不是硬怼测试。
  • 跨工具比较百分比:行/语句/块口径不同,JaCoCo 与 Istanbul 对同一段 代码会给出不同数字;团队应固定一套工具再谈阈值。
  • 排除代码掩盖问题:把难测的类加进 exclude 列表,覆盖率变好看了, 风险只是从报表移进了代码。

小结

  • 初学者要点:语句覆盖最弱;分支覆盖要求判定的真假各一次;条件覆盖看的是 判定内部的原子条件,与分支覆盖互不蕴含;「写测试时至少让每个 if 的两个 方向都发生」是最实用的直觉。
  • 进阶注意:MC/DC 用 n+1 个用例证明每个条件的独立影响,是安全关键领域的 合规要求;工具数字来自不同插桩原理(源码/字节码/引擎原生),阈值要绑定 固定工具;覆盖率是探照灯不是成绩单,断言质量与可测性设计才是根本。