12、代码覆盖率:使用 gcov 和 lcov 测量测试覆盖率,理解行覆盖率、分支覆盖率

说实话,我见过太多团队写了一大堆测试,但心里完全没底。测试到底测了哪些代码?有没有漏掉关键逻辑?

这时候,代码覆盖率工具就派上用场了。它能告诉你——哪些代码被执行过,哪些还在「裸奔」。

我个人习惯把覆盖率比作「测试的探照灯」。灯照到的地方,你心里有数;照不到的地方,就是潜在的雷区。

12.1 什么是代码覆盖率?为什么它重要?

代码覆盖率,简单说就是:你的测试用例执行过程中,覆盖了被测代码的多少比例。

它不是一个「通过/不通过」的指标,而是一个质量信号

核心观点: 覆盖率低不一定代表质量差,但覆盖率高的代码,你重构起来会更有底气。

我在项目中遇到过好几次:某个模块测试全绿,但一上线就崩。后来一查,原来是某个异常分支从来没被测试执行到。嗯,这就是覆盖率的盲区。

12.2 行覆盖率 vs 分支覆盖率

很多人以为覆盖率就是「多少行代码被执行了」。其实这只是最基础的一种。

覆盖率类型 含义 典型工具
行覆盖率 代码中哪些行被执行过 gcov
分支覆盖率 if/else、switch 等分支是否都走过 gcov + lcov
函数覆盖率 哪些函数被调用过 gcov

行覆盖率是最直观的。你写了一个 if 语句,测试只走了 true 分支,那 false 分支的代码行就是「未覆盖」。

分支覆盖率更严格。它关心的是:每个判断点的所有出口是否都被测试到。

我的建议: 行覆盖率可以快速帮你找到「死代码」或「未测代码」。但真正能暴露逻辑漏洞的,是分支覆盖率。

12.3 使用 gcov 测量行覆盖率

gcov 是 GCC 自带的覆盖率工具。不需要额外安装,直接用就行。

基本流程分三步:

  1. 编译时加上特殊选项
  2. 运行测试程序
  3. 用 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 知识体系图:覆盖率的核心逻辑

下面这张图总结了本章的核心脉络:

代码覆盖率 行覆盖率 分支覆盖率 gcov gcov + lcov 文本报告 (.gcov) HTML 可视化报告 图:代码覆盖率工具链与输出形式

12.8 总结与个人建议

代码覆盖率不是银弹,但它是一面镜子。它能照出你测试的盲区,也能帮你建立重构的信心。

我个人习惯把覆盖率目标定在:行覆盖率 85% 以上,分支覆盖率 75% 以上。低于这个线,我会觉得代码「不够安全」。

但记住一点:覆盖率是手段,不是目的。不要为了凑数字去写一堆没意义的测试。真正有价值的,是那些能覆盖到关键分支、边界条件的测试用例。

一句话总结: gcov 告诉你「测了没」,lcov 告诉你「哪里没测」,而你自己要判断「该不该测」。


公众号:蓝海资料掘金营,微信deep3321