12、测试集成:enable_testing、add_test命令、CTest使用、测试覆盖率基础
说到测试,很多人的第一反应是「那是测试工程师的事」。但在我多年的项目经验里,恰恰是开发人员自己写的测试,最能发现代码的深层问题。CMake 内置的 CTest 工具,说白了就是帮你把测试这件事,无缝集成到构建流程里。你不用再手动跑一堆脚本,也不用担心忘了测某个模块。
这一章,我们就来聊聊怎么在 CMake 里把测试搭起来。我会从最基础的命令讲起,再到 CTest 的实际用法,最后聊聊测试覆盖率——嗯,这部分是我个人觉得最有意思的。
12.1 enable_testing:开启测试的大门
在 CMake 里启用测试,其实就一行命令:
enable_testing()
这行命令必须在顶层的 CMakeLists.txt 里调用。为什么?因为 CTest 需要知道整个项目的测试结构,而顶层目录是它的入口点。我曾经在一个子目录里调了 enable_testing(),结果 CTest 死活找不到测试——嗯,踩过这个坑之后,我再也没犯过。
enable_testing() 必须在 add_test() 之前调用,否则测试不会被注册。这个顺序问题,我见过不少新手栽跟头。
12.2 add_test:注册你的测试用例
测试启用之后,下一步就是告诉 CMake:「嘿,这里有个测试要跑」。用 add_test 命令就行:
add_test(NAME <test_name> COMMAND <executable> [args...])
举个例子,假设你有一个可执行文件 test_math,它测试数学库的功能:
add_test(NAME test_math_add COMMAND test_math --add)
add_test(NAME test_math_sub COMMAND test_math --sub)
这里每个测试都有一个名字,方便你后续单独运行或筛选。我个人习惯把测试名字取得有规律一些,比如 模块名_功能名,这样一看就知道测的是什么。
COMMAND 后面跟上就行。CMake 会原样传递给测试程序。
12.3 CTest 使用:跑起来看看
测试注册好了,怎么跑?用 ctest 命令。你可以在构建目录里直接执行:
ctest
这会运行所有注册的测试。如果你想只看某个测试的结果:
ctest -R test_math_add
-R 参数支持正则表达式,非常灵活。比如 ctest -R "math" 会运行所有名字里带 math 的测试。
CTest 还支持并行运行测试:
ctest -j4
这个参数指定同时跑 4 个测试。如果你的测试之间没有依赖关系,并行能大幅缩短总时间。我记得有一次项目里有 200 多个测试,串行跑要 15 分钟,改成 -j8 后,3 分钟就跑完了——效率提升很明显。
CTest 的输出格式也可以定制:
ctest --output-on-failure
这个选项只会在测试失败时输出详细信息。平时跑测试,你只关心哪些挂了,对吧?
12.4 测试覆盖率基础
测试覆盖率,说白了就是「你的代码有多少行被测试执行到了」。覆盖率越高,你越有信心代码没大问题。但注意,100% 覆盖率不代表没有 bug——我见过覆盖率 95% 的项目,核心逻辑还是出了岔子。
在 CMake 里集成覆盖率工具,通常用 gcov 和 lcov。你需要先开启编译器的覆盖率选项:
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
然后编译、运行测试,再用 lcov 生成报告:
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage_report
这样会生成一个 HTML 报告,你可以用浏览器打开,直观地看到哪些代码被覆盖了,哪些没有。
12.5 知识体系图
下面这张图展示了本章的核心逻辑:从 enable_testing 开启测试,到 add_test 注册用例,再到 CTest 运行和覆盖率分析,整个流程一目了然。
12.6 实战建议
最后,分享几个我在项目中积累的经验:
- 测试要早写:不要等代码写完了再补测试。我习惯在写功能代码的同时,就把测试用例写好。这样能及时发现设计上的问题。
- 测试要独立:每个测试用例应该能独立运行,不依赖其他测试的结果或顺序。否则,一个测试挂了,后面一串跟着挂,排查起来很痛苦。
- 覆盖率不是万能的:别被数字迷惑。我见过覆盖率 90% 的项目,核心逻辑的边界条件根本没测到。覆盖率报告要结合代码审查一起看。
- CI 里跑测试:把 CTest 集成到持续集成流水线里,每次提交代码都自动跑一遍。这样能尽早发现问题,而不是等到上线前才手忙脚乱。
CMakeLists.txt 里,我会把测试相关的配置单独放在一个 if(BUILD_TESTING) 块里。这样用户可以通过 -DBUILD_TESTING=OFF 跳过测试构建,节省编译时间。
好了,这一章的内容就到这里。测试集成这件事,说白了就是「把测试变成构建的一部分」。你不需要记住所有命令的细节,但一定要理解这个流程:开启测试、注册用例、运行测试、分析覆盖率。把这四步走通了,你的项目质量就有了基本的保障。