14、测试框架集成:enable_testing与add_test、集成CTest、使用ctest命令运行测试并生成报告

说实话,很多开发者在CMake里写测试,就是简单加两行代码跑通了事。但真正到了项目大了、模块多了、跨平台了,你会发现——测试集成这件事,远不止「能跑」那么简单。

我最早接触CTest是在一个嵌入式项目里。当时团队有十几个人,每个人都在本地跑测试,结果五花八门。有人环境没配好,有人漏了依赖库,还有人测试用例压根没编译进去。后来我花了一周时间,把整个测试流程用CMake+CTest统一管理起来。嗯,从那以后,再也没人跟我说「我本地能跑啊」这种话了。

这一章,我们就来聊聊怎么把测试框架真正「焊」进你的构建系统里。

14.1 测试的起点:enable_testing

在CMake里启用测试,只需要一行命令:

enable_testing()

但这一行放在哪,有讲究。

关键规则:

  • enable_testing() 必须在顶级 CMakeLists.txt 中调用
  • 它只能在 project() 之后调用
  • 一旦调用,整个构建树都会知道「我们要跑测试了」
注意: 如果你在子目录的 CMakeLists.txt 里调用 enable_testing(),CTest 不会生效。我见过有人把测试写在子模块里,结果 ctest 命令跑完显示「0 tests passed」——排查了半天才发现是 enable_testing 放错了位置。

14.2 注册测试用例:add_test

启用测试之后,下一步就是告诉CMake:你的测试用例在哪、怎么跑。

add_test(NAME <test_name> COMMAND <executable> [args...])

举个例子:

enable_testing()

# 假设我们有一个单元测试可执行文件
add_executable(test_math test_math.cpp)
target_link_libraries(test_math PRIVATE MathLib)

# 注册为CTest测试
add_test(NAME MathTest COMMAND test_math)

这里有个细节:COMMAND 后面跟的是可执行文件的名字。CMake会自动帮你找到构建目录下的对应二进制文件。

带参数的测试:

add_test(NAME MathTestWithArgs COMMAND test_math --verbose --iterations 100)

我个人习惯把测试参数写在 CMakeLists.txt 里,而不是硬编码在测试代码中。这样换环境时改起来方便,不用重新编译测试代码。

14.3 集成CTest:让测试跑得更聪明

CTest 是 CMake 自带的测试驱动工具。它不关心你用什么测试框架——Google Test、Catch2、Boost.Test,甚至你自己写的测试脚本,它都能管。

CTest 的核心能力:

  • 自动发现所有通过 add_test 注册的测试
  • 并行执行测试(-j 参数)
  • 超时控制(--timeout
  • 输出格式控制(XML、JUnit等)
  • 支持测试标签和分组

来看一个更完整的例子:

cmake_minimum_required(VERSION 3.16)
project(MyProject LANGUAGES CXX)

enable_testing()

# 添加测试可执行文件
add_executable(unit_test test_main.cpp test_foo.cpp test_bar.cpp)
target_link_libraries(unit_test PRIVATE MyLibrary GTest::GTest GTest::Main)

# 注册测试
add_test(NAME UnitTests COMMAND unit_test)

# 设置超时:每个测试最多跑30秒
set_tests_properties(UnitTests PROPERTIES TIMEOUT 30)

# 给测试打标签
set_tests_properties(UnitTests PROPERTIES LABELS "unit;core")
小技巧: 如果你用 Google Test,可以配合 gtest_discover_tests() 自动发现所有 TEST 宏定义的用例。这样你只需要注册一个可执行文件,CTest 会自动拆分成多个测试用例来报告。

14.4 使用 ctest 命令运行测试

配置和构建完成后,在构建目录下执行:

ctest

就这么简单。但实际工作中,我几乎不会裸跑 ctest。下面是我常用的几种模式:

常用参数组合:

命令 说明
ctest -j4 4个任务并行跑,适合多核机器
ctest --output-on-failure 只显示失败测试的详细输出
ctest -R "Math" 只跑名字包含"Math"的测试
ctest -L "unit" 只跑标签为"unit"的测试
ctest --timeout 60 每个测试超时60秒
ctest --test-dir build 指定构建目录

我曾经在一个CI流水线上遇到过一个问题:测试跑着跑着就卡死了,整个流水线挂在那半小时。后来加了 --timeout 120,问题立刻暴露——有个测试用例死循环了。你看,有时候不是测试框架不行,是你没给它设个「闹钟」。

14.5 生成测试报告

光跑测试还不够,你得能「看见」结果。CTest 支持多种输出格式:

# 生成XML报告(Jenkins等CI工具常用)
ctest --output-junit report.xml

# 生成详细的文本日志
ctest --output-log test_log.txt

# 生成Dashboard提交用的XML
ctest -D Experimental

实战场景: 我在一个跨平台项目中,需要同时在Windows、Linux、macOS上跑测试。我写了一个简单的脚本:

#!/bin/bash
# 在每个平台上统一执行
mkdir -p build && cd build
cmake ..
cmake --build .
ctest --output-junit test_report.xml --output-on-failure -j$(nproc)

然后让CI系统收集 test_report.xml,直接生成可视化的测试趋势图。你想想看,每次提交代码后,测试通过率、耗时变化一目了然——这才是测试集成的真正价值。

14.6 知识体系总览

下面这张图,把本章的核心逻辑串起来了:

CTest 测试集成流程 enable_testing() 在顶级CMakeLists.txt中启用 add_test() 注册测试用例 set_tests_properties() 设置超时/标签等属性 cmake --build . 编译测试可执行文件 ctest -j4 --output-on-failure --output-junit report.xml 控制台输出(通过/失败) XML/JUnit报告(CI集成)

从图中可以看到,整个流程分三步:配置 → 构建 → 运行。每一步都有对应的CMake命令或ctest参数来精细控制。

14.7 避坑指南

最后,分享几个我踩过的坑:

  • 测试找不到动态库: 如果你的测试链接了动态库,记得在 add_test 之前设置 CMAKE_RUNTIME_OUTPUT_DIRECTORY,或者用 set_tests_properties 设置 ENVIRONMENT 变量。
  • 测试路径问题: 测试的工作目录默认是构建目录。如果你的测试需要读取数据文件,要么用绝对路径,要么在 add_test 里用 WORKING_DIRECTORY 指定。
  • 并行测试的资源冲突: 多个测试同时写同一个文件?加锁或者给每个测试分配独立的工作目录。

核心要点回顾:

  • enable_testing() 必须在顶级 CMakeLists.txt 中调用
  • add_test() 注册测试,set_tests_properties() 精细控制
  • CTest 支持并行、超时、标签过滤、多种报告格式
  • 测试报告是CI/CD流水线的关键一环

好了,这一章的内容就到这里。测试集成这件事,说白了就是「让机器替你盯着代码质量」。配置好之后,每次构建自动跑一遍,比人工检查靠谱多了。


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