测试覆盖率的正确姿势:从行数崇拜到风险覆盖

"覆盖率 85%“是测试报告里最常被引用、也最容易被滥用的数字。行数覆盖率 100% 的系统照样天天出事故——覆盖的是"代码被执行",不是"行为被验证"。这篇讲覆盖率的三种形态、各自的正确用法,以及怎么从"行数崇拜"走到"风险覆盖”。

一、先说清楚:行数覆盖率度量的是什么

1
2
3
# coverage.py 的标准产出
name stmts miss cover
order_service.py 240 12 95% ← 240 条语句,228 条被执行过

它只回答一个问题:哪些代码被测试执行到了。它不回答:

  • 执行到的分支断言了什么?(执行了 if x: refund() 但没断言 refund 的行为 = 伪覆盖)
  • 没执行的代码重要吗?(未覆盖的 12 条可能是死代码,也可能是资金逻辑)
  • 关键路径测透了吗?(一个分支覆盖 95%,但核心分支的断言只有 assert status != None

行数覆盖率是必要非充分条件——它的正确角色是"发现盲区",不是"质量证明"。

二、三种覆盖率,各管一件事

1. 代码覆盖率(行数/分支):找盲区

1
pytest --cov=src --cov-branch --cov-report=term-missing

正确用法:

  • 看 miss 列表,不看百分比:85% 本身无意义,order.py:142-158 未覆盖 才有意义——逐条判断"这段为什么没覆盖,该不该覆盖";
  • 分支覆盖比行覆盖诚实if 只测了真分支,行覆盖 100%,分支覆盖 50%——资金/状态机代码强制开 --cov-branch
  • 基线 + 增量:存量代码覆盖率是历史债(可能 40%),门禁卡"增量覆盖率"(新增/改动代码 ≥ 80%),存量只要求"不下降"——卡全量 80% 的结果是没人动老代码,也没人补老代码的用例。

2. 需求覆盖率:测全了没有

1
2
3
4
5
6
7
需求覆盖率 = 被至少一条用例覆盖的需求点 / 需求点总数

需求点拆解(评审时产出,进用例平台):
需求 R-128 "订单支付":
R-128.1 正常支付成功 → 用例 T-201 ✅
R-128.2 支付超时取消 → 用例 T-202 ✅
R-128.3 并发支付幂等 → 用例 ??? ❌ ← 需求覆盖率 67%

需求覆盖率是"完整性"的度量(对应用例设计那篇的完整性层),它和代码覆盖率正交:代码覆盖率 90% + 需求覆盖率 60% = “测得很细但漏了整个功能分支”。

3. 风险覆盖率:测得值不值

1
2
风险覆盖率 = 被用例覆盖的风险项 / 风险清单
风险项 = 失效模式 × 影响面(来自:历史缺陷归因 + 架构依赖分析 + 本次改动 blast radius)

风险覆盖率是三者里最"软"但最贴近决策的:发版评审问的不是"覆盖率多少",是"本次改动的风险面,哪些测了、哪些没测、没测的理由是什么"

三、让覆盖率"可用"的工程动作

1. 覆盖率进 CI,但门禁分级

1
2
3
4
5
6
7
8
9
10
11
12
# CI 覆盖率门禁(pytest-cov 的 fail_under 分模块配置)
coverage:
global:
fail_under: 70 # 整体兜底
modules:
payment:
fail_under: 90 # 资金模块加严
branch: true # 分支覆盖强制
admin_ui:
fail_under: 50 # 内部工具放宽(别用同一把尺子)
diff:
fail_under: 80 # 增量(本次 MR 改动代码)

门禁的目的是"引导用例投在哪",不是"卡人"——资金模块 90%、管理后台 50%,差异本身就是测试策略的声明。

2. 覆盖率趋势图 + 盲区清单(不是数字看板)

1
2
3
4
5
覆盖率看板只放两样:
1. 增量覆盖率趋势(每次 MR 的增量值,连成线)
→ 连续下降 = 团队在用例上偷懒的前兆
2. 盲区 Top 10(按"未覆盖代码 × 模块风险级"排序)
→ 订单支付模块 42 行未覆盖 > 报表导出模块 120 行未覆盖

"数字 + 排序"才驱动行动,裸百分比只是给领导看的。

3. 伪覆盖检测:执行了但没断言 = 0 分

1
2
3
4
5
6
7
8
9
# 反模式:这个用例贡献了代码覆盖,没贡献行为验证
def test_pay():
result = pay(order)
assert result is not None # ← 伪断言:只验证"返回了东西"

# 检测规则(CI lint):
# - assert 必须含具体值/属性(断言黑名单:is not None / is True 单独使用)
# - 每个 @critical 用例至少 2 个业务断言
# - "覆盖但无断言"的代码行单独统计 → 伪覆盖率

伪覆盖率比不覆盖更危险——它制造"很安全"的错觉。CI 里加断言 lint,把"执行覆盖"和"断言覆盖"分开统计。

四、和自动化体系打通:覆盖率数据的三个出口

1
2
3
4
                    ┌→ 门禁:增量覆盖率 < 80% → MR 不合并
测试运行 ── 覆盖率 ──┼→ 风险评级:模块覆盖率 × 缺陷密度 → 用例优先级重排
└→ 报告:L2 定位层展示"本次改动的覆盖情况"(研发视角:
我改的 30 行,28 行有用例覆盖,2 行是配置分支人工验证)

覆盖率给研发看的版本和给测试看的不同:研发关心"我的改动被测没测",所以按"本次 diff × 覆盖"展示——覆盖率数据第一次对"改代码的人"有意义,而不是只考核"写用例的人"。

五、覆盖率永远不该回答的问题

问题 为什么覆盖率答不了
“系统质量好不好” 覆盖率只度量"测了多少",不度量"测得对不对"(断言质量)
“能不能发版” 发版决策看风险敞口(风险覆盖率 + 缺陷状态),不看行数
“哪个团队测试做得好” 用绝对值横向比较 = 鼓励刷分(写无用例凑覆盖是真实发生的)

刷分案例(真实见过):为凑覆盖写了 200 个"只调用不断言"的用例,覆盖率 85% → 96%,三个月后同类缺陷照出——度量会塑造行为,卡错指标就收获刷分

六、小结

覆盖率的正确姿势:行数覆盖找盲区(看 miss 不看百分比)、分支覆盖验逻辑(资金模块强制)、需求覆盖验完整(评审拆解需求点)、风险覆盖验值得(发版决策依据)、断言质量防伪覆盖(执行 ≠ 验证)。覆盖率是镜子不是奖状——它照出"没测到哪",但"测得值不值"永远是风险模型和断言质量的事。