第十四章:测试框架集成——enable_testing()、add_test()、CTest与覆盖率
说实话,测试这件事,很多C++开发者一开始都不太重视。我早年带团队时,就见过一个项目,代码编译全过,一跑就崩,最后定位到是某个模块的边界条件没处理。从那以后,我就把测试集成写进了CMake的标配流程里。
这一章,我们来聊聊CMake里怎么把测试玩明白。说白了,就是三件事:注册测试、运行测试、看覆盖率。
14.1 开启测试:enable_testing()
在CMake里,测试不是默认开启的。你得先告诉构建系统:“嘿,我要跑测试了。” 这个动作就是 enable_testing()。
核心规则:enable_testing() 必须在顶级 CMakeLists.txt 中调用,且必须在 add_test() 之前。
我个人习惯把它放在项目配置之后、目标定义之前。举个例子:
cmake_minimum_required(VERSION 3.16)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)
enable_testing() # 开启测试支持
add_subdirectory(src)
add_subdirectory(tests) # 测试代码单独放一个目录
为什么要在顶级目录调用?因为CTest需要知道整个项目的测试结构。如果你在子目录里调用 enable_testing(),CTest可能找不到测试用例。嗯,这里要注意,我见过有人踩过这个坑。
14.2 注册测试:add_test()
测试开启后,下一步就是告诉CMake:“这个可执行文件是一个测试用例。” 用 add_test() 来注册。
基本语法很简单:
add_test(NAME <test_name> COMMAND <executable> [arg1 arg2 ...])
举个例子,假设你写了一个单元测试 test_math:
add_executable(test_math test_math.cpp)
target_link_libraries(test_math PRIVATE my_math_lib)
add_test(NAME MathTest COMMAND test_math)
这样,当你运行 ctest 时,它就会自动执行 test_math 这个程序。
小技巧: 测试名称最好有规律,比如 ModuleName_TestName。我在项目中就用 Math_AddTest、Math_SubTest 这种命名,方便筛选和排错。
14.3 运行测试:CTest
测试注册好了,怎么跑?用 ctest 命令。它会在构建目录下扫描所有注册的测试,然后逐个执行。
基本用法:
# 先构建
cmake -B build
cmake --build build
# 再测试
cd build
ctest
输出大概长这样:
Test project /path/to/build
Start 1: MathTest
1/1 Test #1: MathTest ...................... Passed 0.02 sec
100% tests passed, 0 tests failed out of 1
CTest 还支持很多参数,我常用的几个:
| 参数 | 作用 |
|---|---|
-V |
显示详细输出,包括测试程序自己的打印 |
-N |
只列出测试,不执行 |
-R |
按正则表达式筛选测试名 |
-j |
并行执行测试,比如 -j4 |
--output-on-failure |
只在失败时输出详细信息 |
举个例子,我只想跑数学相关的测试:
ctest -R Math -V
这样只会执行名字里带 Math 的测试,而且会显示详细输出。调试时特别好用。
14.4 测试覆盖率
测试跑通了,但测了多少代码?这就是覆盖率要回答的问题。CMake 本身不直接生成覆盖率报告,但它可以配合 gcov 和 lcov 来搞定。
基本步骤:
- 编译时开启覆盖率标志
- 运行测试
- 用
gcov或lcov生成报告
在CMake里,我一般这样配置:
option(ENABLE_COVERAGE "Enable coverage reporting" OFF)
if(ENABLE_COVERAGE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage -g -O0")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
endif()
然后构建时开启:
cmake -B build -DENABLE_COVERAGE=ON
cmake --build build
cd build
ctest
测试跑完后,用 lcov 生成HTML报告:
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage_report
打开 coverage_report/index.html,就能看到哪些代码被覆盖了,哪些没被覆盖。
注意: 覆盖率不是越高越好。我见过有人为了100%覆盖率,写了一堆无意义的测试用例。说白了,覆盖率是工具,不是目标。重点测核心逻辑和边界条件。
14.5 知识体系总览
这一章的内容,我用一张图来总结:
14.6 避坑指南
最后,分享几个我踩过的坑:
- 测试路径问题: 测试程序的工作目录是构建目录,不是源码目录。如果你在测试里读取文件,记得用绝对路径或者通过CMake传递路径参数。
- 并行测试冲突: 如果多个测试写同一个文件,并行执行时会互相干扰。我一般给每个测试分配独立的临时目录。
- 覆盖率数据不更新: 有时候改了代码,覆盖率报告还是旧的。记得先
make clean再重新构建,或者用lcov --zerocounters清零。
好了,这一章的内容就到这里。测试集成这件事,说白了就是“先注册、再运行、最后看覆盖”。把这三步走顺了,项目的质量就有了基本保障。