12、代码覆盖率入门:gcov与lcov工具介绍、生成覆盖率报告

各位同学,今天我们来聊聊代码覆盖率。说实话,我刚开始做嵌入式开发那会儿,对覆盖率这玩意儿是有点不屑的。觉得“我代码都跑通了,还要看什么覆盖率?”直到有一次,一个看似稳定的模块,在客户现场出了个极其隐蔽的bug——某个异常分支从来没被触发过,而那个分支恰好有逻辑错误。嗯,从那以后,我再也不敢忽视覆盖率了。

说白了,代码覆盖率就是一把尺子。它告诉你:你的测试用例到底跑了多少代码?哪些代码是“被遗忘的角落”?今天我们就来学习两个最经典的工具——gcovlcov

12.1 什么是代码覆盖率?

代码覆盖率,衡量的是测试执行过程中,源代码被覆盖到的比例。常见的覆盖率指标有:

  • 语句覆盖率:每行代码是否被执行过。
  • 分支覆盖率:每个if-else、switch-case的分支是否都走到过。
  • 函数覆盖率:每个函数是否被调用过。
  • 条件覆盖率:每个布尔子表达式是否取过true和false。

我个人习惯,在单元测试阶段至少盯住语句覆盖率分支覆盖率。这两个指标最直观,也最容易发现问题。

核心观点:覆盖率不是越高越好,但低于80%的语句覆盖率,基本说明你的测试用例没写到位。

12.2 gcov:GCC自带的覆盖率利器

gcov是GCC编译器自带的工具,不需要额外安装。你只要在编译时加上几个特殊选项,它就能帮你记录代码的执行情况。

12.2.1 基本用法

假设我们有一个简单的C文件 calc.c

// calc.c
#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int max(int a, int b) {
    if (a > b) {
        return a;
    } else {
        return b;
    }
}

int main() {
    int x = add(3, 5);
    int y = max(10, 7);
    printf("x=%d, y=%d\n", x, y);
    return 0;
}

编译时加上 -fprofile-arcs -ftest-coverage 选项:

gcc -fprofile-arcs -ftest-coverage calc.c -o calc

运行生成的可执行文件:

./calc

这时目录下会生成 calc.gcdacalc.gcno 文件。然后执行:

gcov calc.c

你会看到输出:

File 'calc.c'
Lines executed:85.71% of 7
Creating 'calc.c.gcov'

打开 calc.c.gcov 文件,每一行前面会显示该行被执行了多少次:

        -:    0:Source:calc.c
        -:    1:#include <stdio.h>
        -:    2:
        -:    3:int add(int a, int b) {
        1:    4:    return a + b;
        -:    5:}
        -:    6:
        -:    7:int max(int a, int b) {
        1:    8:    if (a > b) {
        1:    9:        return a;
        -:   10:    } else {
    #####:   11:        return b;
        -:   12:    }
        -:   13:}
        -:   14:
        -:   15:int main() {
        1:   16:    int x = add(3, 5);
        1:   17:    int y = max(10, 7);
        1:   18:    printf("x=%d, y=%d\n", x, y);
        1:   19:    return 0;
        -:   20:}

看到第11行前面的 ##### 了吗?这表示该行从未被执行过。因为我们的测试用例中 max(10, 7) 走的是 a > b 分支,else 分支被漏掉了。

小技巧:我建议每次跑完测试,都看一眼.gcov文件里有没有 ##### 标记。如果有,说明有代码没测到,赶紧补用例。

12.2.2 常用选项

选项 说明
-b 显示分支覆盖率信息
-f 显示每个函数的覆盖率
-o 指定目标文件目录

例如:

gcov -b -f calc.c

会额外输出每个函数的分支覆盖情况。

12.3 lcov:让覆盖率报告更直观

gcov的输出是文本文件,看多了眼睛疼。lcov就是来解决这个问题的——它能把gcov的数据转换成漂亮的HTML报告。

12.3.1 安装lcov

在Ubuntu/Debian上:

sudo apt-get install lcov

在macOS上:

brew install lcov

12.3.2 生成HTML报告

还是用上面的 calc.c 例子。先确保已经生成了 .gcda.gcno 文件,然后执行:

# 第一步:收集覆盖率数据
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,你会看到一个漂亮的网页:

  • 顶部显示总体覆盖率百分比。
  • 左侧是文件列表,每个文件旁边有覆盖率进度条。
  • 点击文件名,可以看到每一行代码的覆盖情况——绿色表示已覆盖,红色表示未覆盖。
注意:我曾经遇到过一个问题——明明重新编译了,但覆盖率数据还是旧的。这是因为gcda文件没有清理干净。每次重新测试前,记得执行 make clean 或者手动删除所有 .gcda 文件。

12.4 实战:一个完整的覆盖率流程

我们用一个稍微复杂点的例子,演示完整的流程。假设项目结构如下:

project/
├── src/
│   ├── math.c
│   └── math.h
└── test/
    └── test_math.c

math.c 内容:

// math.c
#include "math.h"

int divide(int a, int b) {
    if (b == 0) {
        return -1;  // 错误处理
    }
    return a / b;
}

int is_even(int n) {
    return (n % 2 == 0);
}

test_math.c 内容:

// test_math.c
#include "math.h"
#include <assert.h>

int main() {
    assert(divide(10, 2) == 5);
    assert(divide(10, 0) == -1);
    assert(is_even(4) == 1);
    assert(is_even(7) == 0);
    return 0;
}

编译并运行测试:

gcc -fprofile-arcs -ftest-coverage src/math.c test/test_math.c -o test_runner
./test_runner

生成覆盖率报告:

lcov --capture --directory . --output-file coverage.info
lcov --remove coverage.info '/usr/*' --output-file coverage_filtered.info
genhtml coverage_filtered.info --output-directory coverage_report

打开报告,你会看到 math.c 的语句覆盖率是100%,分支覆盖率也是100%。因为我们的测试用例覆盖了 b == 0b != 0 两个分支,以及 n % 2 == 0 的true和false两种情况。

避坑指南:我曾经在嵌入式项目中,因为目标板资源有限,没法直接在板上跑lcov。我的做法是:在宿主机上用模拟器跑单元测试,生成覆盖率数据,再生成报告。虽然不能完全代表真实硬件上的执行路径,但至少能保证逻辑层面的覆盖。

12.5 知识体系图

下面这张图总结了本章的核心逻辑:

代码覆盖率工具链工作流程 C源码 + 测试用例 gcc -fprofile-arcs -ftest-coverage 运行测试可执行文件 生成 .gcda .gcno gcov 生成 .gcov 文本 lcov 收集数据 genhtml 生成HTML报告 分析报告,补充测试用例 迭代:补充用例 → 重新编译 → 重新测试

12.6 常见问题与避坑

  1. gcda文件没生成? 检查程序是否正常退出。如果程序被 kill -9 或者异常崩溃,gcda文件可能写不完整。
  2. 覆盖率数据不准? 确保每次重新编译前清理了旧的 .gcda 文件。我习惯在Makefile里加一个 clean_coverage 目标。
  3. 嵌入式环境怎么用? 交叉编译时,在宿主机上编译带覆盖率选项的代码,然后在目标板上运行,把生成的 .gcda 文件拷回宿主机,再用lcov处理。
  4. 报告里全是红色? 别慌,先检查是不是过滤掉了系统头文件。如果不过滤,/usr/include 下的代码也会被统计,那覆盖率肯定惨不忍睹。
我的习惯:每次提交代码前,我都会跑一遍覆盖率报告,确保新增代码的覆盖率不低于90%。如果低于这个数,我会反问自己:是测试用例没写够,还是代码本身有不可测的死角?

好了,关于gcov和lcov的基础用法就讲到这里。这两个工具组合起来,能让你对代码的测试情况一目了然。记住,覆盖率不是目的,而是手段——它的真正价值,是帮你找到那些你没想到要测的代码路径。


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