第32章:测试与CTest:enable_testing()和add_test(),运行单元测试
说实话,很多C++项目写完了,编译通过,跑起来也没报错,但你真的敢说它没问题吗?
我以前也不敢。直到我把CTest用起来,才觉得心里踏实多了。
这一章,我们就聊聊CMake内置的测试工具——CTest。
说白了,它帮你把测试用例组织起来,一键运行,还能看结果汇总。
32.1 为什么需要CTest?
我见过不少项目,测试代码写了一大堆,但没人跑。
为什么?因为没有统一的入口。
有人用gtest直接编译运行,有人写了个shell脚本,还有人干脆把测试代码注释掉了。
CTest解决的就是这个问题:
它让所有测试用例,都能通过 make test 或 ctest 命令统一执行。
你不需要记住每个测试的可执行文件叫什么,也不需要手动一个个点开。
32.2 启用测试:enable_testing()
在CMake中启用测试,只需要一行:
enable_testing()
这行代码必须写在顶级 CMakeLists.txt 中。
为什么?因为CTest需要从顶层开始递归查找测试用例。
如果你写在子目录里,CTest可能找不到。
我个人习惯把它放在项目最开始,紧挨着 project() 之后:
cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0)
enable_testing()
add_subdirectory(src)
add_subdirectory(tests)
32.3 添加测试:add_test()
有了测试框架,接下来就是添加具体的测试用例了。
add_test() 的语法很简单:
add_test(NAME <test_name> COMMAND <executable> [args...])
举个例子:
add_test(NAME test_math COMMAND test_math_exe)
这里 test_math_exe 是一个可执行文件,它内部包含了测试逻辑。
CTest会运行它,然后根据返回值判断是否通过:返回0表示通过,非0表示失败。
add_test(NAME test_math COMMAND test_math_exe --gtest_filter="Add*"),只运行名字以Add开头的测试。
32.4 一个完整的例子
我们来看一个实际项目中的用法。
假设项目结构如下:
project/
├── CMakeLists.txt
├── src/
│ ├── CMakeLists.txt
│ └── math.cpp
└── tests/
├── CMakeLists.txt
└── test_math.cpp
顶级 CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(MathLib VERSION 1.0)
enable_testing()
add_subdirectory(src)
add_subdirectory(tests)
tests/CMakeLists.txt:
add_executable(test_math_exe test_math.cpp)
target_link_libraries(test_math_exe PRIVATE MathLib)
add_test(NAME test_math_add COMMAND test_math_exe --test=add)
add_test(NAME test_math_sub COMMAND test_math_exe --test=sub)
这样,当你运行 make test 或 ctest 时,CTest会自动执行这两个测试用例。
32.5 测试框架的选择
CTest本身不写测试代码,它只负责运行。
测试代码怎么写?你可以用任何框架:
| 框架 | 特点 | 我是否推荐 |
|---|---|---|
| Google Test | 功能强大,社区活跃 | 强烈推荐 |
| Catch2 | 单头文件,轻量 | 适合小项目 |
| Boost.Test | 功能全面,但依赖Boost | 看情况 |
| 自定义main函数 | 简单粗暴 | 不推荐,除非你很清楚自己在做什么 |
我个人最常用Google Test。它在CMake中集成非常方便:
find_package(GTest REQUIRED)
add_executable(test_math_exe test_math.cpp)
target_link_libraries(test_math_exe PRIVATE GTest::GTest GTest::Main)
add_test(NAME test_math COMMAND test_math_exe)
32.6 运行测试的几种方式
配置好之后,运行测试有三种常见方式:
- make test:最直观,但需要先make编译。
- ctest:直接运行,CTest会自动编译并执行测试。
- ctest -V:显示详细输出,适合调试。
我记得有一次,测试一直失败,但看不到具体错误信息。
后来发现是忘了加 -V 参数。加上之后,错误日志一目了然。
ctest 发现测试没跑,先检查 enable_testing() 是否在顶级CMakeLists.txt中,并且是否在 add_subdirectory() 之前。
32.7 测试的组织与命名规范
项目大了,测试用例会很多。
我建议你给测试命名时,遵循一定的规范:
- 模块名_功能名:比如
math_add、math_sub - 测试名要能反映测试内容:比如
test_add_positive_numbers - 用下划线分隔:避免空格带来的麻烦
我曾经接手过一个项目,测试命名乱七八糟,有叫 test1 的,有叫 check 的。
结果想跳过某个测试时,根本不知道哪个是哪个。
后来我花了一整天重新整理命名,才把测试管理起来。
32.8 测试的过滤与分组
CTest支持通过标签(LABELS)对测试进行分组:
add_test(NAME test_math_add COMMAND test_math_exe)
set_tests_properties(test_math_add PROPERTIES LABELS "math;core")
然后你可以只运行某个标签的测试:
ctest -L math
或者排除某个标签:
ctest -LE slow
这个功能在大型项目中特别有用。
比如你只改了数学模块,就可以只跑 math 标签的测试,节省大量时间。
32.9 测试的超时与资源管理
有些测试可能跑得很慢,或者陷入死循环。
CTest提供了超时机制:
set_tests_properties(test_math_add PROPERTIES TIMEOUT 10)
超过10秒,CTest会强制终止这个测试,并标记为失败。
另外,如果测试需要特定环境变量,可以用 ENVIRONMENT 属性:
set_tests_properties(test_math_add PROPERTIES ENVIRONMENT "MY_VAR=hello")
32.10 知识体系图
下面这张图总结了CTest的核心流程:
32.11 避坑总结
最后,我把这些年踩过的坑总结一下:
- enable_testing() 位置不对:必须在顶级CMakeLists.txt中,且在add_subdirectory之前。
- 测试可执行文件没链接对库:测试代码需要链接被测试的库,否则编译通过但运行崩溃。
- 忘记加 -V 参数:看不到错误详情,排查问题很痛苦。
- 测试命名冲突:不同模块的测试同名,CTest会覆盖前面的。
- 测试超时未设置:死循环的测试会卡住整个CI流程。
嗯,CTest其实不复杂,但用好了,能让你的项目质量上一个台阶。
从今天开始,给你的项目加上测试吧。
公众号:蓝海资料掘金营,微信 deep3321