23、CMake测试集成:enable_testing、add_test、CTest、测试依赖管理
测试这件事,说实话,很多C++项目一开始都不太重视。我早年带团队时,也踩过这个坑——代码越写越多,回归测试全靠人工点一遍,最后谁都不敢改代码。后来我强制自己在CMake里把测试框架搭好,才算是把项目从泥潭里拉了出来。
今天我们就聊聊CMake里的测试集成。说白了,就是怎么用CMake自带的CTest工具,把你的单元测试、集成测试管起来,让它们能一键运行、自动报告结果。
enable_testing:开启测试能力的开关
在CMake里启用测试,第一步就是调用enable_testing()。这个命令必须在顶级CMakeLists.txt里调用,而且只能调用一次。我见过有人把它写在子目录里,结果CTest死活不认——嗯,这是个很容易犯的错。
# 顶级CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyProject LANGUAGES CXX)
enable_testing() # 开启测试支持
add_subdirectory(src)
add_subdirectory(tests)
调用之后,CMake就会生成CTest需要的配置文件。你可以在构建目录里找到CTestTestfile.cmake,里面记录了所有注册的测试。
add_test:注册你的测试用例
有了测试能力,接下来就是往里面加测试。add_test的用法很直接:
add_test(NAME test_name COMMAND executable [args...])
举个例子,假设你有一个单元测试的可执行文件叫test_math:
# tests/CMakeLists.txt
add_executable(test_math test_math.cpp)
target_link_libraries(test_math PRIVATE MathLib GTest::GTest)
add_test(NAME MathTest COMMAND test_math)
这里有个细节:COMMAND后面跟的是可执行文件的名称。CMake会自动解析构建目标,找到对应的可执行文件路径。如果你直接写路径,反而容易出问题。
MathTest、NetworkTest。这样在CTest里可以按模式筛选运行,比如 ctest -R Math 只跑数学相关的测试。
CTest:运行测试的利器
测试注册好了,怎么跑?直接用ctest命令。在构建目录里执行:
mkdir build && cd build
cmake ..
cmake --build .
ctest # 运行所有测试
CTest会逐个执行你注册的测试,并收集结果。默认输出是这样的:
Test project /path/to/build
Start 1: MathTest
1/2 Test #1: MathTest ....................... Passed 0.02 sec
Start 2: NetworkTest
2/2 Test #2: NetworkTest ................... Passed 0.15 sec
100% tests passed, 0 tests failed out of 2
我个人最常用的几个CTest参数:
-R <regex>:只运行匹配正则的测试-E <regex>:排除匹配正则的测试-j N:并行运行N个测试,能大幅缩短总时间--output-on-failure:失败时打印测试输出,调试利器-V或-VV:显示详细输出
# 并行跑4个测试,只跑Math相关的
ctest -j 4 -R Math --output-on-failure
测试依赖管理:谁先跑谁后跑
有时候测试之间有依赖关系。比如集成测试需要先跑单元测试,确保基础功能没问题。CMake提供了add_test的DEPENDS参数:
add_test(NAME UnitTest COMMAND test_unit)
add_test(NAME IntegrationTest COMMAND test_integration)
set_tests_properties(IntegrationTest PROPERTIES DEPENDS UnitTest)
这样设置后,CTest会先跑UnitTest,如果它失败了,IntegrationTest会被标记为Not Run,不会执行。这个机制在大型项目里特别有用——你想想看,如果底层模块都挂了,上层测试跑也是白跑。
除了依赖,还可以设置超时时间:
set_tests_properties(NetworkTest PROPERTIES TIMEOUT 30)
如果测试超过30秒没结束,CTest会强制终止并标记为失败。我在项目中遇到过一些测试因为网络问题卡死,加了超时后,CI流程稳定多了。
测试标签与分组
当测试数量多起来,按标签分组是个好习惯。你可以给测试打上标签,然后按标签筛选运行:
add_test(NAME MathAddTest COMMAND test_math --gtest_filter=Add*)
set_tests_properties(MathAddTest PROPERTIES LABELS "math;unit")
add_test(NAME MathMulTest COMMAND test_math --gtest_filter=Mul*)
set_tests_properties(MathMulTest PROPERTIES LABELS "math;unit")
add_test(NAME NetworkHttpTest COMMAND test_network --gtest_filter=Http*)
set_tests_properties(NetworkHttpTest PROPERTIES LABELS "network;integration")
然后你可以这样运行:
# 只跑单元测试
ctest -L unit
# 只跑网络相关的测试
ctest -L network
# 排除集成测试
ctest -LE integration
测试覆盖率与自定义配置
CTest还支持自定义属性。比如你想记录测试的预期耗时、负责人信息:
set_tests_properties(MathTest PROPERTIES
COST 2.0
FIXTURES_REQUIRED "math_fixture"
ENVIRONMENT "TEST_MODE=ci"
)
COST属性告诉CTest这个测试大概耗时2秒,CTest在并行调度时会尽量平衡负载。FIXTURES_REQUIRED用于声明测试需要的fixture,fixture可以是一个启动数据库、启动mock server等前置操作。
SVG:CMake测试集成知识体系
实战:一个完整的测试集成示例
最后,我放一个完整的例子。假设你有一个数学库,用Google Test写单元测试:
# 顶级 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MathLib LANGUAGES CXX)
enable_testing()
add_subdirectory(src)
add_subdirectory(tests)
# src/CMakeLists.txt
add_library(MathLib math.cpp)
target_include_directories(MathLib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
# tests/CMakeLists.txt
find_package(GTest REQUIRED)
include(GoogleTest)
add_executable(test_math test_math.cpp)
target_link_libraries(test_math PRIVATE MathLib GTest::GTest GTest::Main)
# 注册测试
add_test(NAME MathAddTest COMMAND test_math --gtest_filter=Add*)
add_test(NAME MathMulTest COMMAND test_math --gtest_filter=Mul*)
# 设置标签
set_tests_properties(MathAddTest PROPERTIES LABELS "math;unit")
set_tests_properties(MathMulTest PROPERTIES LABELS "math;unit")
# 设置依赖:MulTest 依赖 AddTest
set_tests_properties(MathMulTest PROPERTIES DEPENDS MathAddTest)
# 使用 gtest_discover_tests 自动发现所有测试(推荐)
gtest_discover_tests(test_math
PROPERTIES
LABELS "math;unit"
)
注意最后用了gtest_discover_tests,它会自动扫描Google Test的测试用例,每个用例注册为一个CTest测试。这样你就不用手动写add_test了,省事很多。我个人现在基本都用这个方式,除非有特殊需求才手动注册。
好了,CMake测试集成这块的核心内容就这些。从enable_testing开启能力,到add_test注册用例,再到CTest运行和依赖管理,每一步都不复杂,但组合起来就能搭出一个靠谱的测试基础设施。你想想看,如果每次提交代码后,CTest能自动跑一遍所有测试,失败的发邮件通知——这得省下多少人工回归的时间。