25. 7.1 CTest测试框架

引言:施工队长的“质检员”副职

在前面的章节里,我们的 CMake “施工队长”已经带领各支小队(Target)完成了从地基(编译)到封顶(链接)的全部工作,甚至把大楼交付给了业主(Install)。但一栋真正合格的大楼,在交付前必须经过严格的质检:混凝土强度够不够?电路通不通?防水漏不漏?

在 C++ 项目里,这份“质检报告”就是自动化测试。如果每次改完代码都要手动运行一遍程序、瞪大眼睛看输出,那不仅效率低下,而且极易遗漏隐患。CMake 自带了一套名为 CTest 的测试框架,它就像施工队长兼任的“质检科主管”——不需要你额外雇人(安装新工具),只要告诉他“质检标准是什么”“按什么顺序检查”“哪几项可以并行检测”,他就能在构建完成后自动帮你跑完所有测试,并生成一份漂亮的报告。

这一节,我们就来学习如何把 CTest 纳入你的构建体系,从零搭建一套可筛选、可编排、可加速的自动化测试流水线。

启用测试:enable_testing 与 include(CTest) 的区别

要让 CMake 开启测试功能,首先需要在 CMakeLists.txt 中“挂牌成立质检科”。这里有两个看似相似、实则分工不同的指令:

enable_testing:最小化启动

enable_testing() 是 CTest 的“电源开关”。只要在项目根目录的 CMakeLists.txt 中调用一次,CMake 就会在当前及子目录范围内启用测试注册机制。它非常轻量,不引入任何额外变量或目标,只负责声明:本项目支持测试

cmake_minimum_required(VERSION 3.20)
project(MyProject)

enable_testing()  # 打开测试开关

add_subdirectory(src)
add_subdirectory(tests)

include(CTest):全能型质检中心

include(CTest) 则是一个“豪华套餐”。它在背后做了两件事:

  1. 自动调用 enable_testing(),所以你不需要再写一遍;
  2. 引入一系列高级功能,例如自动生成 BUILD_TESTING 选项(用户可以通过 -DBUILD_TESTING=OFF 一键关闭所有测试编译)、支持 CDash 提交、内置 Coverage 和 MemCheck 等测试模式。
cmake_minimum_required(VERSION 3.20)
project(MyProject)

include(CTest)  # 自动 enable_testing(),并增加 CDash 等支持

# 用户可通过 -DBUILD_TESTING=OFF 跳过测试构建
if(BUILD_TESTING)
    add_subdirectory(tests)
endif()

如何选择?

对于零基础或中小型项目,推荐直接使用 include(CTest)。它多出来的 BUILD_TESTING 开关非常实用,能让你的 CI/CD 脚本或同事在只想编译主程序时跳过测试代码。如果你在做极简嵌入式项目,且确定不需要任何 CTest 周边生态,那么 enable_testing() 就够了。

添加测试:add_test 的语法与命令指定

质检科挂牌后,接下来要“添加质检项目”。add_test 就是那张质检工单,它告诉 CTest:运行什么命令、起什么名字、参考什么标准

基本语法

add_test(NAME <测试名> COMMAND <命令> [<参数>...])

最经典的用法是把一个可执行目标注册为测试:

add_executable(calc_test calc_test.cpp)
target_link_libraries(calc_test PRIVATE calc_lib)

add_test(NAME CalcBasicTest COMMAND calc_test)

这里 COMMAND 后面跟的 calc_test 既可以是 CMake 目标名(CMake 会自动解析成实际的可执行文件路径),也可以是系统上的任意命令,比如 Python 脚本、Shell 脚本,甚至是另一条 CMake 命令。

带参数的命令

如果你的测试可执行文件需要命令行参数(例如 GoogleTest 的过滤器),直接跟在后面即可:

add_test(NAME CalcAdvancedTest COMMAND calc_test --gtest_filter=CalcTest.*)

使用 WORKING_DIRECTORY

默认情况下,测试会在构建目录(${CMAKE_CURRENT_BINARY_DIR})下执行。如果测试需要读取数据文件,可以切换工作目录:

add_test(NAME DataDrivenTest COMMAND python3 run_tests.py
         WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/test_data)

命令解析器陷阱与 COMMAND_EXPAND_LISTS

在较老的 CMake 版本中,COMMAND 里的列表变量可能会被分词截断。CMake 3.16 引入了 COMMAND_EXPAND_LISTS 选项,可以确保变量被正确展开:

set(MY_ARGS arg1 arg2 arg3)
add_test(NAME ExpandedTest COMMAND my_test ${MY_ARGS} COMMAND_EXPAND_LISTS)

测试属性精细控制:set_tests_properties

仅仅“能跑起来”还不够,质检科需要更精细的规则:如果测试预期就是会失败怎么办?输出必须包含什么关键字才算通过?测试需要特定的环境变量吗?这些都可以通过 set_tests_properties 来设置。

语法概览

set_tests_properties(<测试名1> <测试名2> ... PROPERTIES <属性名1> <值1> <属性名2> <值2> ...)

常用属性速查

  • WILL_FAIL TRUE:预期该测试会失败。如果它真的失败了,CTest 反而标记为通过;如果它意外通过了,则标记为失败。常用于测试“非法输入是否被正确拦截”。
  • PASS_REGULAR_EXPRESSION “正则表达式”:测试输出必须匹配该正则才算通过。适合检查命令行工具的打印信息。
  • FAIL_REGULAR_EXPRESSION “正则表达式”:输出一旦匹配该正则,立即判定为失败。例如检查程序是否输出了 Segmentation fault
  • SKIP_REGULAR_EXPRESSION “正则表达式”(CMake 3.16+):输出匹配时,测试被标记为跳过(Skipped),不计入失败。
  • ENVIRONMENT “VAR1=Value1;VAR2=Value2”:为测试单独设置环境变量,多个变量用分号隔开。
  • TIMEOUT 秒数:设置超时时间,防止死循环测试卡死整个流程。
  • COST 数值:给测试标注“运行成本”,成本高的测试在并行执行时会被优先分配,以优化总耗时(详见后文)。
  • DEPENDS 其他测试名:控制执行顺序,确保被依赖的测试先跑完。
  • LABELS “标签1;标签2”:为测试打标签,方便后续按类别筛选。
  • FIXTURES_SETUP / FIXTURES_CLEANUP / FIXTURES_REQUIRED(CMake 3.7+):更现代的测试依赖和资源准备机制,适合需要“先启动服务,再跑测试,最后清理”的场景。

完整示例

add_test(NAME ServerStartup COMMAND start_server.sh)
set_tests_properties(ServerStartup PROPERTIES FIXTURES_SETUP MyServerFixture)

add_test(NAME ApiTest COMMAND api_test_client)
set_tests_properties(ApiTest PROPERTIES FIXTURES_REQUIRED MyServerFixture)

add_test(NAME ServerShutdown COMMAND stop_server.sh)
set_tests_properties(ServerShutdown PROPERTIES FIXTURES_CLEANUP MyServerFixture)

# 另一个例子:预期失败 + 输出检查
add_test(NAME BadInputTest COMMAND calc_test --input="bad_data")
set_tests_properties(BadInputTest PROPERTIES WILL_FAIL TRUE)

# 环境变量与超时
add_test(NAME NetTest COMMAND net_test --host=127.0.0.1)
set_tests_properties(NetTest PROPERTIES
    ENVIRONMENT "TEST_PORT=8080;DEBUG=1"
    TIMEOUT 30
    COST 5.0
)

测试分组与标签:LABELS 筛选

当项目膨胀到几十甚至上百个测试时,你肯定不希望每次调试都跑完全部。LABELS 就是测试的“科室分类牌”。

为测试打标签

add_test(NAME MathUnitTest COMMAND math_test)
set_tests_properties(MathUnitTest PROPERTIES LABELS "unit;math;fast")

add_test(NAME MathBenchmark COMMAND math_bench)
set_tests_properties(MathBenchmark PROPERTIES LABELS "perf;math;slow")

add_test(NAME NetIntegrationTest COMMAND net_test)
set_tests_properties(NetIntegrationTest PROPERTIES LABELS "integration;network;slow")

按标签运行

在命令行中,使用 ctest -L(包含标签)或 -LE(排除标签)进行筛选:

# 只跑单元测试
ctest -L unit

# 跑所有 math 相关的测试
ctest -L math

# 排除慢测试,快速验证
ctest -LE slow

# 组合条件:math 标签且不是 slow
ctest -L math -LE slow

在 CI 流水线里,你可以把 ctest -LE slow 放在提交前的快速检查中,把完整的 ctest 放在夜间构建(Nightly Build)里。

测试依赖与顺序控制:DEPENDS

某些测试天然具有先后顺序:比如必须先跑 InitDatabase 初始化测试数据库,才能跑 UserCrudTest。虽然 Modern CMake 更推荐使用 FIXTURES 机制来管理这种“资源准备-使用-清理”的生命周期,但 DEPENDS 属性依然是一种简单直接的顺序控制手段。

DEPENDS 基本用法

add_test(NAME Step1_Init COMMAND init_db)
add_test(NAME Step2_Insert COMMAND insert_test)
add_test(NAME Step3_Query COMMAND query_test)

set_tests_properties(Step2_Insert PROPERTIES DEPENDS Step1_Init)
set_tests_properties(Step3_Query PROPERTIES DEPENDS Step2_Insert)

这样,即使你用 ctest -j 并行跑测试,CTest 也会保证 Step1_Init 先完成,Step2_Insert 随后,最后是 Step3_Query

DEPENDS 的局限性

DEPENDS 只保证执行顺序,不保证“资源传递”。如果 Step1 启动了一个临时服务器,Step2 去连接它,那么服务器的生命周期管理最好用 FIXTURES 来做,因为 FIXTURES 能确保即使中间某个测试崩溃,清理脚本(FIXTURES_CLEANUP)依然会被执行。

超时与成本:TIMEOUT 和 COST 的并行优化

测试数量一多,串行执行就会像单窗口排队一样让人抓狂。CTest 支持通过 -j 参数并行跑测试,但并行不是无脑加速——如果不加调度,可能会出现“一个重型测试独占线程,其他轻量测试干等”的低效局面。

TIMEOUT:防止卡死的保险丝

网络测试或涉及线程的测试最容易陷入死锁。TIMEOUT 属性就是那条保险丝:

add_test(NAME HangingRiskTest COMMAND risky_op)
set_tests_properties(HangingRiskTest PROPERTIES TIMEOUT 10)

如果 10 秒内没跑完,CTest 会强制终止该测试,并标记为 Timeout,而不会拖累整个测试流程。

COST:给测试“称重”,优化并行调度

COST 是一个浮点数值,表示测试的相对运行成本。CTest 在并行执行(ctest -j N)时,会优先启动 COST 更高的测试。这样做的原因是:如果先跑短测试,长测试被留到最后,那么当它运行时,其他线程可能已经空闲,造成总时间浪费;反之,先启动长测试,短测试可以“见缝插针”地在后面补齐,整体耗时更短。

add_test(NAME QuickUnit COMMAND quick_test)
set_tests_properties(QuickUnit PROPERTIES COST 1.0)

add_test(NAME HeavySimulation COMMAND heavy_sim)
set_tests_properties(HeavySimulation PROPERTIES COST 15.0)

add_test(NAME MediumIntegration COMMAND mid_test)
set_tests_properties(MediumIntegration PROPERTIES COST 5.0)

命令行并行执行

# 使用 4 个线程并行跑测试
ctest -j 4

# 并行 + 标签筛选 + 详细输出
ctest -j 4 -L unit --output-on-failure

合理设置 COST 后,你会发现总测试时间显著下降。如果你不确定每个测试的实际耗时,可以先跑一遍 ctest,观察日志里的耗时,再按比例填写 COST

小结:搭建你的第一道质检防线

这一节我们让 CMake 的“施工队长”兼任了“质检科主管”:

  • include(CTest)enable_testing() 挂牌成立测试部门;
  • add_test 注册具体质检项目,支持可执行文件、脚本及带参数的命令;
  • set_tests_properties 精细控制测试行为,包括预期失败、输出检查、环境变量、超时和资源成本;
  • LABELS 给测试分类,实现快速筛选与分级执行;
  • DEPENDS 编排有顺序要求的测试流;
  • TIMEOUTCOST 保障稳定性并加速并行测试。

到目前为止,你已经能写出一个结构清晰、属性完备、可筛选可加速的 CTest 测试套件了。下一节,我们将走进 CTest 的命令行世界,学习如何在终端里灵活驾驭这套质检系统,解读它的输出报告,并把它无缝接入 CI/CD 流水线。

请登录后发表评论

    没有回复内容

正在唤醒异次元光景……