一、为什么需要测试覆盖率?
说实话,很多C++项目跑完单元测试就觉得万事大吉了。但你真的知道哪些代码被测试到了吗?
我在一个嵌入式项目中就吃过这个亏。当时单元测试全部通过,结果上线后某个边缘分支的代码直接崩溃。后来一查,那段代码压根没被任何测试用例覆盖到。嗯,从那以后,测试覆盖率就成了我项目里的标配。
测试覆盖率说白了就是:你的测试用例到底跑了多少代码。它能帮你发现那些「被遗忘的角落」。
二、gcov + lcov 是什么?
gcov 是 GCC 自带的代码覆盖率工具。它不需要额外安装,只要你用 GCC 编译就能用。lcov 则是 gcov 的图形化增强版,能把 gcov 生成的原始数据转成漂亮的 HTML 报告。
我个人习惯这样理解:gcov 是矿工,负责挖数据;lcov 是设计师,负责把数据变成看得懂的图表。
三、核心流程
整个流程其实就三步,我画了张图帮你理清思路:
四、CMake 集成实战
4.1 基础配置
在 CMake 里集成 gcov 其实不复杂。我一般会在项目的顶层 CMakeLists.txt 里加一个选项:
option(ENABLE_COVERAGE "Enable coverage reporting" OFF)
if(ENABLE_COVERAGE)
# 检查编译器是否支持
if(NOT CMAKE_COMPILER_IS_GNUCXX AND NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
message(FATAL_ERROR "Coverage requires GCC or Clang")
endif()
# 添加编译和链接标志
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O0 -fprofile-arcs -ftest-coverage")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -O0 -fprofile-arcs -ftest-coverage")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fprofile-arcs -ftest-coverage")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fprofile-arcs -ftest-coverage")
endif()
-g -O0。不加 -g 的话,gcov 报告里看不到源码行号。不加 -O0 的话,编译器优化可能会让行号对应关系变得混乱。我刚开始就踩过这个坑,报告里的行号完全对不上。
4.2 添加自定义目标
接下来,我习惯加一个自定义目标,一键生成覆盖率报告:
if(ENABLE_COVERAGE)
find_program(LCOV lcov REQUIRED)
find_program(GENHTML genhtml REQUIRED)
# 清理旧的覆盖率数据
add_custom_target(clean-coverage
COMMAND ${LCOV} --directory ${CMAKE_BINARY_DIR} --zerocounters
COMMENT "Cleaning coverage counters"
)
# 收集覆盖率数据并生成报告
add_custom_target(coverage
COMMAND ${LCOV} --capture --directory ${CMAKE_BINARY_DIR}
--output-file ${CMAKE_BINARY_DIR}/coverage.info
--rc lcov_branch_coverage=1
COMMAND ${LCOV} --remove ${CMAKE_BINARY_DIR}/coverage.info
"/usr/*" "${CMAKE_SOURCE_DIR}/tests/*" "${CMAKE_SOURCE_DIR}/third_party/*"
--output-file ${CMAKE_BINARY_DIR}/coverage_filtered.info
--rc lcov_branch_coverage=1
COMMAND ${GENHTML} ${CMAKE_BINARY_DIR}/coverage_filtered.info
--output-directory ${CMAKE_BINARY_DIR}/coverage_report
--rc lcov_branch_coverage=1
--title "My Project Coverage Report"
COMMAND ${CMAKE_COMMAND} -E echo "Report generated: ${CMAKE_BINARY_DIR}/coverage_report/index.html"
DEPENDS clean-coverage
COMMENT "Generating coverage report"
)
endif()
--remove 这一步很重要。如果不排除系统头文件和第三方库,你的报告会被大量无关代码淹没。我曾经看到一个项目,报告里 90% 都是 STL 的覆盖率,自己的代码反而没几行。
五、实战演示
5.1 项目结构
假设我们有这样一个简单项目:
my_project/
├── CMakeLists.txt
├── src/
│ ├── CMakeLists.txt
│ ├── calculator.cpp
│ └── calculator.h
└── tests/
├── CMakeLists.txt
└── test_calculator.cpp
5.2 构建并生成报告
# 配置时开启覆盖率
mkdir build && cd build
cmake .. -DENABLE_COVERAGE=ON
# 编译
make
# 运行测试
ctest
# 生成覆盖率报告
make coverage
执行完后,打开 build/coverage_report/index.html,你就能看到这样的报告:
| 指标 | 说明 | 我的建议值 |
|---|---|---|
| 行覆盖率 | 被执行的代码行数占比 | ≥ 80% |
| 分支覆盖率 | if/else、switch 等分支的执行情况 | ≥ 70% |
| 函数覆盖率 | 被调用的函数占比 | ≥ 90% |
六、避坑指南
我曾经遇到过几个典型问题,分享给你:
- gcda 文件找不到? 检查一下工作目录。gcda 文件会生成在程序运行时的当前目录,而不是构建目录。我建议在 CMake 里设置
CMAKE_RUNTIME_OUTPUT_DIRECTORY统一管理。 - 报告里全是 ??? 大概率是编译时没加
-g选项。没有调试信息,gcov 就不知道源码在哪。 - 覆盖率数据不更新? 每次重新生成报告前,记得先跑
make clean-coverage清空计数器。不然旧数据会和新数据混在一起。 - 多线程程序覆盖率不准? gcov 对多线程支持有限。如果代码里有线程竞争,覆盖率数据可能会乱。我一般会在单线程模式下跑覆盖率测试。
七、进阶技巧
7.1 合并多个测试的覆盖率
如果你的项目有多个测试可执行文件,可以分别收集再合并:
# 分别收集
lcov --capture --directory test1_build --output-file test1.info
lcov --capture --directory test2_build --output-file test2.info
# 合并
lcov --add-tracefile test1.info --add-tracefile test2.info --output-file total.info
# 生成报告
genhtml total.info --output-directory combined_report
7.2 设置覆盖率阈值
我习惯在 CI 里加一个阈值检查,低于某个值就报错:
# 检查行覆盖率是否低于 80%
lcov --summary coverage_filtered.info | grep "lines" | awk '{if($2 < 80) exit 1}'
八、总结
gcov + lcov 这套组合拳,成本低、效果好。你不需要安装任何第三方工具,GCC 自带就够用。配合 CMake 的选项控制,开发时关掉、CI 时打开,非常灵活。
我个人建议:每个项目至少跑一次覆盖率报告。你可能会惊讶地发现,有些你以为是「核心代码」的地方,其实从来没被测试过。嗯,这就是覆盖率报告的价值所在。
最后提醒一句:报告生成后,别只看总数字。点进去看看哪些文件覆盖率低,想想为什么。是测试用例没写够?还是那段代码本身就有问题?这才是覆盖率分析的正确姿势。
公众号:蓝海资料掘金营,微信deep3321