导语
在前面的章节中,我们已经系统掌握了 CMake 的构建、安装、打包与发布流程。一个项目能编译通过、能打出安装包,只是完成了“能用”的第一步。真正决定软件质量的,是测试。试想,如果没有自动化测试,每次修改代码后你都只能手动运行可执行文件来验证,这不仅低效,更无法覆盖复杂的边界场景。
CMake 深知这一点,因此它内置了一套完整的测试编排框架——CTest。CTest 并不是某个独立的第三方工具,而是 CMake 官方提供的测试驱动程序。它能自动发现测试、串并行执行、过滤筛选、超时控制,并与 CI/CD 流程无缝集成。从本节开始,我们将进入 CMake 项目的“质量保障”环节,手把手教你如何用 CTest 将测试体系化。
启用测试:enable_testing 与 include(CTest)
要在 CMake 项目中使用测试,第一步永远是“启用测试功能”。CMake 提供了两种启用方式,很多初学者会混淆,我们一次讲清。
轻量级启用:enable_testing()
enable_testing() 是最纯粹、最常用的启用方式。它只做一件事:在当前目录及子目录范围内启用测试支持。调用后,你就可以使用 add_test() 命令注册测试用例,构建系统也会生成一个名为 test 的伪目标(例如 make test 或 cmake --build . --target test)。
cmake_minimum_required(VERSION 3.20)
project(MyProject)
# 核心:启用测试
enable_testing()
add_executable(my_app main.cpp)
add_test(NAME AppSmokeTest COMMAND my_app --smoke-test)
完整版启用:include(CTest)
include(CTest) 是“大而全”的方案。它在内部同样会调用 enable_testing(),但额外做了三件事:
- 引入 CDash 支持:生成
Nightly、Experimental、Continuous等构建目标,方便将测试结果提交到 CDash 仪表板。 - 读取配置文件:自动查找项目根目录下的
CTestConfig.cmake,获取项目站点、提交策略等元信息。 - 扩展 ctest 行为:提供更多内置变量和模块级控制(例如默认测试超时、内存检查配置等)。
cmake_minimum_required(VERSION 3.20)
project(MyProject)
# 完整启用:包含 CDash 与扩展功能
include(CTest)
add_executable(my_app main.cpp)
add_test(NAME AppSmokeTest COMMAND my_app --smoke-test)
如何选择?
给大家一个简洁的决策建议:
- 绝大多数项目:使用
enable_testing()即可。它干净、无侵入,只专注于“跑测试”。 - 需要 CDash 集成或大型开源项目:使用
include(CTest)。如果你的项目需要向公共仪表板提交测试结果,或者深度使用 CTest 的分布式测试特性,再考虑这个。
在本教程的后续示例中,我们统一使用 enable_testing(),以保证示例的纯粹性。
添加测试:add_test 命令详解
启用测试后,就可以通过 add_test() 命令向 CTest 注册测试用例了。它的作用类似于告诉 CTest:“请帮我执行这条命令,并根据它的退出码判断测试是否通过。”
基本语法结构
Modern CMake 推荐的标准语法如下:
add_test(NAME <测试名> COMMAND <命令> [<参数>...])
其中:
NAME:测试的唯一标识名。这个名字将出现在ctest的输出报告中,也用于后续设置属性时的引用。COMMAND:要执行的命令。它可以是编译生成的可执行目标名、系统命令、脚本,甚至是 CMake 内置命令。
指定测试命令的多种方式
CTest 的灵活性体现在 COMMAND 的多样性上。看下面这个综合示例:
enable_testing()
# 方式1:直接引用 CMake 目标(推荐)
add_executable(unit_test_math tests/test_math.cpp)
target_link_libraries(unit_test_math mathlib)
add_test(NAME MathUnit COMMAND unit_test_math)
# 方式2:带命令行参数
add_test(NAME MathWithVerbose COMMAND unit_test_math --verbose --gtest_filter=AddTest.*)
# 方式3:调用 CMake 内置命令(常用于拷贝数据、预处理)
add_test(NAME PrepareData COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_SOURCE_DIR}/data/input.txt
${CMAKE_BINARY_DIR}/test_input.txt
)
# 方式4:执行系统命令或脚本
add_test(NAME PythonValidation COMMAND python3 ${CMAKE_SOURCE_DIR}/scripts/validate.py)
注意方式1:当 COMMAND 后面直接跟一个 CMake 可执行目标名时,CTest 会自动将其解析为构建后的完整路径(例如 ./unit_test_math 或 unit_test_math.exe),无需你手动处理路径差异,这是跨平台项目中最安全的写法。
设置工作目录
有时测试程序需要从特定目录读取相对路径的配置文件。这时可以通过属性设置工作目录:
add_test(NAME ConfigLoadTest COMMAND config_test)
set_tests_properties(ConfigLoadTest PROPERTIES
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests/fixtures
)
测试属性精控:set_tests_properties
如果只是简单注册测试,那 add_test 就够了。但工程实践中,你需要控制超时、环境变量、并行资源、输出匹配等细节。set_tests_properties() 就是 CTest 的“精细遥控器”。
常用核心属性一览
以下是一张在工程中最常用、也最值得掌握的属性清单:
LABELS:给测试打标签(如unit、slow),用于分类筛选。TIMEOUT:测试超时时间(秒),防止死循环测试阻塞整个流程。COST:测试的相对“成本”估计值,用于指导并行调度。PROCESSORS:声明该测试需要占用多少 CPU 核心,防止过度并行。DEPENDS:声明测试间的执行顺序依赖。ENVIRONMENT:为测试进程设置专属环境变量。WILL_FAIL:如果设为TRUE,则测试进程返回非零退出码时,CTest 反而认为“通过”。PASS_REGULAR_EXPRESSION:只要 stdout/stderr 输出匹配该正则,即算通过。FAIL_REGULAR_EXPRESSION:只要输出匹配该正则,即算失败。SKIP_REGULAR_EXPRESSION:输出匹配时,测试被标记为“跳过”而非失败。FIXTURES_SETUP / FIXTURES_CLEANUP / FIXTURES_REQUIRED:现代化“夹具”机制,用于有状态依赖的测试组。
输出匹配与预期失败
有些测试程序不会通过退出码区分结果,而是打印 PASS 或 FAIL。此时可以用正则匹配:
add_test(NAME LegacyTest COMMAND old_test_runner)
set_tests_properties(LegacyTest PROPERTIES
PASS_REGULAR_EXPRESSION "TEST PASSED"
FAIL_REGULAR_EXPRESSION "TEST FAILED;ERROR;Segmentation fault"
)
再比如,你有一个测试是用来验证程序“确实会在非法输入时崩溃”,这个测试预期会返回非零值:
add_test(NAME CrashOnNull COMMAND app --input-null)
set_tests_properties(CrashOnNull PROPERTIES WILL_FAIL TRUE)
环境变量与资源声明
测试经常需要临时目录、数据库连接字符串等环境配置:
add_test(NAME DBIntegration COMMAND test_db)
set_tests_properties(DBIntegration PROPERTIES
ENVIRONMENT "DB_HOST=localhost;DB_PORT=5432;TEST_DATA_DIR=${CMAKE_SOURCE_DIR}/data"
TIMEOUT 30
PROCESSORS 2
)
测试分类管理:LABELS 标签系统
随着项目膨胀,测试数量会从几个增长到几百个。你不可能每次都跑全量测试,尤其是在本地开发时只想快速验证改动。LABELS 就是为解决这个问题而生的。
给测试打上标签
一个测试可以拥有多个标签,用分号 ; 分隔:
add_executable(test_fast tests/test_fast.cpp)
add_executable(test_slow tests/test_slow.cpp)
add_executable(test_io tests/test_io.cpp)
add_test(NAME FastMath COMMAND test_fast)
add_test(NAME SlowTraining COMMAND test_slow)
add_test(NAME DiskIO COMMAND test_io)
set_tests_properties(FastMath PROPERTIES LABELS "unit;fast")
set_tests_properties(SlowTraining PROPERTIES LABELS "integration;slow;gpu")
set_tests_properties(DiskIO PROPERTIES LABELS "unit;io;slow")
命令行筛选实战
配置好标签后,在构建目录下使用 ctest 命令即可灵活筛选:
# 只运行带有 "unit" 标签的测试
ctest -L unit
# 排除带有 "slow" 标签的测试(本地快速冒烟)
ctest -LE slow
# 组合使用:只跑 unit 标签里的 fast 子集(如果 fast 也是单独标签)
ctest -L "unit" -LE "slow"
# 查看所有测试及其标签列表(不执行)
ctest -N
这里的 -L 是 Label 的缩写,-LE 是 Label Exclude 的缩写。-N 非常有用,它让你在不实际运行的情况下预览 CTest 发现了哪些测试。
测试执行顺序:DEPENDS 依赖控制
默认情况下,CTest 会按照测试注册的顺序串行执行(并行模式下则无序调度)。如果你的测试有状态依赖——例如“必须先初始化数据库,再跑查询测试,最后清理数据”——就需要显式声明依赖关系。
使用 DEPENDS 属性
add_test(NAME DB_Setup COMMAND init_database)
add_test(NAME DB_Query COMMAND test_queries)
add_test(NAME DB_Cleanup COMMAND cleanup_database)
# 声明执行链:Query 依赖 Setup;Cleanup 依赖 Query
set_tests_properties(DB_Query PROPERTIES DEPENDS DB_Setup)
set_tests_properties(DB_Cleanup PROPERTIES DEPENDS DB_Query)
上述配置保证了:
DB_Setup一定会先于DB_Query执行。DB_Query完成后,才会执行DB_Cleanup。
多对一依赖
一个测试也可以依赖多个前置测试,用分号分隔:
add_test(NAME FinalReport COMMAND generate_report)
set_tests_properties(FinalReport PROPERTIES
DEPENDS "DB_Query;API_Validation;Cache_Warmup"
LABELS "report"
)
重要提示:DEPENDS 只控制执行顺序,不控制测试失败后的行为。如果 DB_Setup 失败了,DB_Query 默认仍会被标记为 Not Run,但 CTest 整体会继续执行后续不相关的测试。如果你希望“一失败就全部停止”,可以在命令行使用 ctest --stop-on-failure。
更现代的替代:Fixture(夹具)
对于复杂的 Setup/Teardown 场景,CMake 3.7+ 提供了更专业的 FIXTURES_SETUP、FIXTURES_REQUIRED 和 FIXTURES_CLEANUP。不过理解 DEPENDS 是掌握 Fixture 的基础,本节先夯实这个基本概念。
并行与性能:TIMEOUT 与 COST
在 CI/CD 环境中,测试数量大、时间宝贵。CTest 原生支持并行测试执行,而 TIMEOUT 和 COST 就是并行调度的两把钥匙。
防止卡死:TIMEOUT
网络测试、死锁测试、或者某些有概率陷入无限循环的代码,都可能导致测试“挂死”。TIMEOUT 属性为单个测试设置了死刑:
add_test(NAME NetworkHandshake COMMAND test_network)
set_tests_properties(NetworkHandshake PROPERTIES TIMEOUT 15)
add_test(NAME InfiniteLoopRisk COMMAND test_heavy)
set_tests_properties(InfiniteLoopRisk PROPERTIES TIMEOUT 60)
如果测试执行时间超过设定的秒数,CTest 会强制终止该进程,并将测试标记为 Timeout。你也可以通过变量 CTEST_TEST_TIMEOUT 设置全局默认值:
set(CTEST_TEST_TIMEOUT 10) # 默认10秒超时
并行调度优化:COST 与 PROCESSORS
当你使用 ctest -jN 开启并行测试时,CTest 需要决定“先调度哪个测试、同时跑几个”。COST 和 PROCESSORS 就是给它提供的调度 hints。
- COST:一个相对的数值,表示该测试的预计耗时。数值越大,表示测试越“重”。CTest 的调度器会参考这个值来优化并行策略,尽量让长测试先启动,短测试填补空隙,从而减少总耗时。
- PROCESSORS:声明该测试需要占用多少个 CPU 逻辑核心。如果某个测试会启动 4 个线程做计算,你应该设置
PROCESSORS 4,这样 CTest 在并行时就不会在同一时间安排过多资源密集型测试,避免系统 thrashing。
add_test(NAME QuickMath COMMAND test_quick) # 预计1秒
add_test(NAME ModelTrain COMMAND test_train) # 预计30秒,多线程
add_test(NAME FullRender COMMAND test_render) # 预计60秒,多线程
set_tests_properties(QuickMath PROPERTIES
COST 1
TIMEOUT 5
)
set_tests_properties(ModelTrain PROPERTIES
COST 30
TIMEOUT 120
PROCESSORS 4
)
set_tests_properties(FullRender PROPERTIES
COST 60
TIMEOUT 300
PROCESSORS 8
)
命令行实践
在构建目录中,你可以这样调用 CTest:
# 串行执行全部测试(默认)
ctest
# 并行执行:同时最多跑8个测试,并根据 COST/PROCESSORS 智能调度
ctest -j8
# 只跑 unit 标签的测试,8并行,失败时立即输出日志
ctest -L unit -j8 --output-on-failure
# 按成本降序显示测试清单(辅助你调整 COST 值)
ctest -N
其中 --output-on-failure 是本地调试的神器:只要测试失败,CTest 会立即把该测试的 stdout/stderr 打印到终端,省去你翻日志文件的麻烦。
小节总结
本节我们系统梳理了 CTest 测试框架的入门核心:
- 用
enable_testing()或include(CTest)启用测试能力,绝大多数场景下前者更轻量。 - 用
add_test(NAME ... COMMAND ...)注册测试,命令可以是 CMake 目标、系统命令或脚本。 - 用
set_tests_properties()精细控制测试行为,包括超时、环境变量、正则匹配、预期失败等。 - 用
LABELS对测试进行分类,配合ctest -L实现灵活筛选。 - 用
DEPENDS控制测试执行顺序,处理有状态依赖的测试链。 - 用
TIMEOUT防止挂死,用COST和PROCESSORS指导并行调度,最大化 CI 效率。
掌握了这些,你的 CMake 项目就已经具备了工业级的自动化测试骨架。在下一节中,我们将继续深入,学习如何在命令行更高效地运行测试、分析测试结果,以及与代码覆盖率工具的联动。


没有回复内容