12、代码覆盖率:使用 gcov 和 lcov 测量测试覆盖率,理解行覆盖率、分支覆盖率
说实话,我见过太多团队写了一大堆测试,但心里完全没底。测试到底测了哪些代码?有没有漏掉关键逻辑?
这时候,代码覆盖率工具就派上用场了。它能告诉你——哪些代码被执行过,哪些还在「裸奔」。
我个人习惯把覆盖率比作「测试的探照灯」。灯照到的地方,你心里有数;照不到的地方,就是潜在的雷区。
12.1 什么是代码覆盖率?为什么它重要?
代码覆盖率,简单说就是:你的测试用例执行过程中,覆盖了被测代码的多少比例。
它不是一个「通过/不通过」的指标,而是一个质量信号。
核心观点: 覆盖率低不一定代表质量差,但覆盖率高的代码,你重构起来会更有底气。
我在项目中遇到过好几次:某个模块测试全绿,但一上线就崩。后来一查,原来是某个异常分支从来没被测试执行到。嗯,这就是覆盖率的盲区。
12.2 行覆盖率 vs 分支覆盖率
很多人以为覆盖率就是「多少行代码被执行了」。其实这只是最基础的一种。
| 覆盖率类型 | 含义 | 典型工具 |
|---|---|---|
| 行覆盖率 | 代码中哪些行被执行过 | gcov |
| 分支覆盖率 | if/else、switch 等分支是否都走过 | gcov + lcov |
| 函数覆盖率 | 哪些函数被调用过 | gcov |
行覆盖率是最直观的。你写了一个 if 语句,测试只走了 true 分支,那 false 分支的代码行就是「未覆盖」。
分支覆盖率更严格。它关心的是:每个判断点的所有出口是否都被测试到。
我的建议: 行覆盖率可以快速帮你找到「死代码」或「未测代码」。但真正能暴露逻辑漏洞的,是分支覆盖率。
12.3 使用 gcov 测量行覆盖率
gcov 是 GCC 自带的覆盖率工具。不需要额外安装,直接用就行。
基本流程分三步:
- 编译时加上特殊选项
- 运行测试程序
- 用 gcov 生成报告
来看一个例子。假设我们有这样一个函数:
// calculator.cpp
int divide(int a, int b) {
if (b == 0) {
return -1; // 错误处理
}
return a / b;
}
编译时加上 -fprofile-arcs -ftest-coverage:
g++ -fprofile-arcs -ftest-coverage calculator.cpp -o calculator_test
运行测试后,会生成 .gcda 和 .gcno 文件。然后执行:
gcov calculator.cpp
你会得到一个 calculator.cpp.gcov 文件,里面每一行前面都有数字:
-: 0:Source:calculator.cpp
-: 1:#include <iostream>
1: 2:int divide(int a, int b) {
1: 3: if (b == 0) {
0: 4: return -1;
1: 5: }
1: 6: return a / b;
-: 7:}
看到第4行前面的 0 了吗?这说明 b == 0 这个分支从未被执行过。
注意: gcov 默认只显示行覆盖率。要查看分支覆盖率,需要额外加参数 --branch-probabilities。
12.4 使用 lcov 生成可视化报告
gcov 的输出是文本文件,看起来不够直观。lcov 可以把这些数据转换成 HTML 页面,带颜色标记,一目了然。
基本用法:
# 收集覆盖率数据
lcov --capture --directory . --output-file coverage.info
# 过滤掉系统头文件
lcov --remove coverage.info '/usr/*' --output-file coverage_filtered.info
# 生成 HTML 报告
genhtml coverage_filtered.info --output-directory coverage_report
打开 coverage_report/index.html,你会看到:
- 绿色:已覆盖
- 红色:未覆盖
- 黄色:部分覆盖(比如 if 只走了 true 分支)
我个人习惯把 lcov 报告放在 CI 的构建产物里,每次提交代码都能看到覆盖率变化。
12.5 分支覆盖率的实战意义
我曾经在一个支付模块里吃过亏。代码长这样:
void processPayment(double amount, bool isVip) {
if (amount > 1000) {
// 大额支付,需要额外审核
if (isVip) {
// VIP 用户免审
approve(amount);
} else {
// 普通用户需要人工审核
manualReview(amount);
}
} else {
// 小额支付,直接通过
approve(amount);
}
}
行覆盖率显示 100%。但分支覆盖率呢?
测试只测了「大额 + VIP」和「小额」两种情况。那个「大额 + 非 VIP」的分支,从来没被触发过。
结果上线后,普通用户的大额支付全部卡住,没人审核。嗯,这就是分支覆盖率低带来的真实事故。
关键教训: 行覆盖率 100% 不代表分支覆盖率 100%。分支覆盖率才是真正考验测试深度的指标。
12.6 覆盖率工具的常见陷阱
用 gcov 和 lcov 时,有几个坑我踩过,分享给你:
- 优化选项的影响: 编译时如果开了
-O2或-O3,编译器可能会内联函数、合并分支,导致覆盖率数据失真。建议测试时用-O0。 - 多线程程序: gcov 对多线程的支持不太好,可能会出现数据竞争。我一般会在单线程模式下跑覆盖率测试。
- 异常处理代码: 很多团队不测异常路径,导致 catch 块覆盖率极低。但这恰恰是最容易出问题的地方。
一个小技巧: 在 CI 中设置覆盖率阈值,比如行覆盖率不低于 80%,分支覆盖率不低于 70%。低于阈值就构建失败。这样能倒逼团队重视测试质量。
12.7 知识体系图:覆盖率的核心逻辑
下面这张图总结了本章的核心脉络:
12.8 总结与个人建议
代码覆盖率不是银弹,但它是一面镜子。它能照出你测试的盲区,也能帮你建立重构的信心。
我个人习惯把覆盖率目标定在:行覆盖率 85% 以上,分支覆盖率 75% 以上。低于这个线,我会觉得代码「不够安全」。
但记住一点:覆盖率是手段,不是目的。不要为了凑数字去写一堆没意义的测试。真正有价值的,是那些能覆盖到关键分支、边界条件的测试用例。
一句话总结: gcov 告诉你「测了没」,lcov 告诉你「哪里没测」,而你自己要判断「该不该测」。