覆盖率与假绿灯:识别无效测试

覆盖率是最容易制造安全感的指标。一个 assert err == nil 的测试就能把行覆盖率推到 100%,而它连返回值是不是空的都没检查。要处理三件事:三种覆盖率口径各自骗你在哪里、无效测试长什么样、怎么让 AI 审查测试本身。

三种口径与它们的欺骗性

口径统计对象常用工具典型欺骗
行覆盖(Line)被执行到的代码行coverage.py 默认、JaCoCo、Istanbul一行里写三个条件,走通一条就算这行已覆盖
语句覆盖(Statement)被执行到的可执行语句go test -cover、-coverprofile短路表达式与空分支也算走通
分支覆盖(Branch)判断的真/假两侧、循环体进出--cov-branch、JaCoCo 分支计数a && b 只统计整个表达式的真/假,不展开条件组合
条件/路径覆盖(MC/DC、Path)条件组合与执行路径少数安全行业工具组合数指数增长,日常项目基本不跑
# Go:语句覆盖统计 + 未覆盖行定位
go test ./... -covermode=atomic -coverprofile=cover.out
go tool cover -func=cover.out | tail -5      # 总覆盖率与函数级明细
go tool cover -html=cover.out                # 在浏览器里看哪些行是红的

# Python:加 --cov-branch 才是分支覆盖,默认只有行覆盖
pytest --cov=src --cov-branch --cov-report=term-missing

一个 100% 行覆盖却什么都没验证的例子:

func Discount(u User) int {
	if u.Vip && u.Age > 60 { // 只测 Vip=false,这一行就算被"覆盖"了
		return 20
	}
	return 0
}

只调用 Discount(User{Vip: false, Age: 0}),行覆盖率是 100%,但 Age > 60 从来没有被求值为真。这就是口径的欺骗性:覆盖率统计的是「代码被执行过」,不是「行为被验证过」。

无效测试的七种长相

模式代码气味检出方法修法
只断言不报错测试体里只有调用,没有断言注释掉被测函数的返回值,看它是否还绿断言可观察结果(返回值、状态、副作用表)
断言恒真assertTrue(x != null)、len(list) >= 0把断言手动改成「必须为假」,看原断言是否本来就不会失败断言具体值与边界区间
同源期望值期望值由被测实现的同一套逻辑算出来看断言右侧是否调用了被测方法或同一私有函数期望值手写字面量
只断言交互verify(mock).save(any())改内部实现(行为不变)是否变红断言最终状态,交互断言只用于副作用本身
异常断言过宽assertRaises(Exception)让实现抛出另一种异常,看是否仍然通过断言具体异常类型与消息
顺序依赖单独跑某条用例就失败随机顺序执行、单条执行每个用例自造数据,不依赖同文件里的其他用例
休眠等待sleep(2) 等异步结果看执行时长与偶发失败率轮询到条件成立,带超时上限

再补一条最容易被忽略的:黄金文件(Golden File)被随手更新。快照类测试的价值在于 diff 必须人工确认,一旦养成「红了就重新生成一次」的习惯,它就退化成了一条永不失败的测试。

让 AI 审查测试本身

审查测试和审查代码是两套提示词:前者关心「断言的可失败性」,后者关心正确性。

下面这些测试全部通过。请审查「测试本身」的质量,不要审查被测代码的实现风格。要求:
1) 逐条列出弱断言与无效断言,格式:文件:行号 | 断言内容 | 为什么它可能永远为真 | 改成什么;
2) 找出「期望值由被测实现的同一套逻辑算出来」的地方——这类测试会跟着 bug 一起错;
3) 找出只断言 err == nil、len(x) > 0、对象非空这类存在性断言,并说明它漏掉了什么;
4) 找出互相依赖、必须按顺序执行才能通过的用例,写清依赖了哪个状态;
5) 给出 3 条「把实现故意改错后测试仍然会通过」的具体改法:改哪一行、为什么不会变红;
6) 这套测试行覆盖率 92%,请直接回答:剩下最容易出问题、却没有被任何断言覆盖的行为是什么?
不要输出「建议补充更多边界用例」这类空话,每条都要引用具体代码。

覆盖率的正确用法是当「下限警报」而不是「目标」:--cov-fail-under 的作用是阻止覆盖率倒退(棘轮,Ratchet),门禁应该盯「本次改动的增量覆盖率」(diff 覆盖率)而不是全仓库数字——历史包袱不该由一次新提交还清。

常见坑

坑后果规避
把覆盖率当考核指标有人写出无断言的测试冲数字门禁盯增量覆盖率,并配合变异测试或人工抽查
用覆盖率证明「测好了」假绿灯,线上照样出事覆盖率只说明代码被执行过,不说明行为被验证过
只测顺利路径分支口径很高,错误路径没走强制要求每条 if err != nil 至少一条用例
为凑覆盖率补测试维护成本上升、信心下降覆盖率下降要写理由,而不是无脑补用例
把变异测试设成必跑门禁执行太久,团队直接绕过只对核心模块抽样运行
删掉失败的测试红灯消失,问题保留失败用例要么修实现,要么修用例并说明理由

小结:覆盖率是「执行过」的度量,不是「验证过」的度量——行覆盖看得见语句、看不见条件,分支覆盖看得见条件、看不见期望值;识别无效测试靠四板斧:让断言有失败的可能(改坏实现看它是否变红)、期望值手写、断言状态而非交互、用例之间互不依赖;可以让 AI 按清单审查测试本身,但最终只有「故意改坏代码,测试会不会红」这个实验能给出结论。

笔记加载中…