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,里面记录了所有注册的测试。

注意:enable_testing() 必须在 add_subdirectory 之前调用,否则子目录里的测试不会被注册。我曾经因为这个排查了半天,最后发现是顺序问题。

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会自动解析构建目标,找到对应的可执行文件路径。如果你直接写路径,反而容易出问题。

小技巧:我习惯给测试命名时加上模块前缀,比如 MathTestNetworkTest。这样在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_testDEPENDS参数:

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
核心思路:测试标签让筛选变得极其灵活。我一般会按「模块名」和「测试级别」(unit/integration/e2e)两个维度打标签,这样CI里可以灵活组合。

测试覆盖率与自定义配置

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测试集成知识体系

CTest 测试框架 enable_testing() 开启测试支持 add_test() 注册测试用例 测试依赖管理 DEPENDS / TIMEOUT 测试标签与分组 LABELS / -L / -LE 自定义属性 COST / FIXTURES ctest 命令运行测试 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了,省事很多。我个人现在基本都用这个方式,除非有特殊需求才手动注册。

我的建议:小项目用手动 add_test 完全够用。项目超过20个测试用例后,强烈建议用 gtest_discover_tests 或类似工具自动发现。否则维护 add_test 列表会变成一件苦差事。

好了,CMake测试集成这块的核心内容就这些。从enable_testing开启能力,到add_test注册用例,再到CTest运行和依赖管理,每一步都不复杂,但组合起来就能搭出一个靠谱的测试基础设施。你想想看,如果每次提交代码后,CTest能自动跑一遍所有测试,失败的发邮件通知——这得省下多少人工回归的时间。

公众号:蓝海资料掘金营,微信 deep3321