16、CMake中的测试支持:enable_testing()、add_test()、ctest命令、测试用例编写与运行

测试这件事,说实话,很多C++项目一开始都不太重视。我早年带团队时也踩过这个坑——代码越写越多,改一个底层模块,上层全崩了,全靠肉眼排查。后来我强制在CMake里把测试框架搭起来,才算是把命救回来了。

CMake对测试的支持其实很轻量,它不规定你用哪个测试框架,也不限制你写测试的方式。它只做三件事:启用测试、注册测试、运行测试。说白了,就是给你一个统一的入口,把测试这件事管起来。

16.1 启用测试:enable_testing()

想在项目里用CMake的测试功能,第一步就是调用 enable_testing()。这个命令必须在顶级CMakeLists.txt里调用,而且要在 add_subdirectory() 之前。为什么?因为测试是全局的,子目录里的测试用例需要注册到顶层。

关键点: enable_testing() 只能在顶级CMakeLists.txt中调用一次。如果你在子目录里调用,CMake会直接报错。

我个人习惯在项目一开始就把这行加上,哪怕当时还没写测试。你想想看,后面加测试只是加几行 add_test() 的事,但要是忘了开这个开关,所有测试注册都会静默失效,排查起来很头疼。

# 顶级 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0.0)

enable_testing()  # 开启测试支持

add_subdirectory(src)
add_subdirectory(tests)

16.2 注册测试:add_test()

测试启用之后,怎么告诉CMake「这个可执行文件是一个测试用例」?用 add_test()

基本语法很简单:

add_test(NAME <test_name> COMMAND <command> [<arg>...])

举个例子,我写了一个单元测试的可执行文件叫 test_math,那么注册方式就是:

add_executable(test_math test_math.cpp)
add_test(NAME MathTest COMMAND test_math)

这里要注意,COMMAND 后面跟的是可执行文件的名字。如果这个可执行文件不在当前构建目录下,你需要指定完整路径,或者用 $<TARGET_FILE:test_math> 这种生成器表达式。

小技巧: 我习惯把测试可执行文件统一放在 tests/ 目录下,然后用 add_subdirectory(tests) 引入。这样顶层CMakeLists.txt很干净,测试的构建和注册逻辑都集中在 tests 目录里。

16.3 测试用例的编写

CMake本身不写测试,它只负责跑。测试用例怎么写,完全取决于你用什么测试框架。我见过团队用Google Test、Catch2、Boost.Test,甚至有人直接用 assert()main() 返回值来搞。

我个人最常用的是Google Test,因为它生态好、断言丰富。但这里我不展开讲某个框架的用法,而是说一个通用的原则:测试可执行文件应该返回0表示通过,非0表示失败。CMake和ctest就是靠这个返回值来判断测试结果的。

举个最简单的例子,不用任何框架:

// test_simple.cpp
#include <cassert>

int add(int a, int b) { return a + b; }

int main() {
    assert(add(2, 3) == 5);
    assert(add(-1, 1) == 0);
    return 0;  // 全部通过
}

然后注册:

add_executable(test_simple test_simple.cpp)
add_test(NAME SimpleTest COMMAND test_simple)

嗯,这里要注意:如果 assert 失败,程序会 abort,返回值是非0,ctest就会判定为失败。简单粗暴,但够用。

16.4 运行测试:ctest命令

测试注册好了,怎么跑?用 ctest 命令。它会在构建目录下读取 CTestTestfile.cmake(这个文件是CMake在配置阶段自动生成的),然后依次执行所有注册的测试。

基本用法:

# 先构建
cmake -B build
cmake --build build

# 进入构建目录运行测试
cd build
ctest

# 或者直接指定构建目录
ctest --test-dir build

输出大概长这样:

Test project /path/to/build
    Start 1: MathTest
1/2 Test #1: MathTest ........................   Passed    0.01 sec
    Start 2: SimpleTest
2/2 Test #2: SimpleTest ......................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 2

看到那个 100% tests passed 的时候,心里还是挺踏实的。

16.5 常用ctest选项

ctest 的选项不少,我挑几个最常用的说说:

选项 作用 示例
-R 按正则表达式过滤测试名 ctest -R Math 只跑名字带Math的测试
-E 排除匹配的测试 ctest -E Slow 跳过名字带Slow的测试
-N 只列出测试,不运行 ctest -N 查看所有注册的测试
-V 详细输出 ctest -V 显示每个测试的完整输出
-j 并行运行测试 ctest -j4 同时跑4个测试
--output-on-failure 只在失败时输出详细信息 ctest --output-on-failure

避坑指南: 我曾经在CI里用 ctest -j 并行跑测试,结果有些测试共享了同一个临时文件,导致随机失败。排查了半天才发现是并行冲突。如果你的测试用例之间有资源竞争,记得加锁或者用 --test-load 限制并行数。

16.6 测试的超时与资源限制

有些测试可能会卡住,比如网络请求超时、死循环等。ctest 提供了超时机制:

add_test(NAME NetworkTest COMMAND test_network)
set_tests_properties(NetworkTest PROPERTIES TIMEOUT 10)

这样如果 test_network 10秒内没跑完,ctest 就会杀掉它并标记为失败。我在做嵌入式交叉编译测试时经常用这个,因为目标板响应慢,不加超时的话CI会卡死。

其他常用属性还有:

  • COST:测试的成本(用于调度,值越大越晚执行)
  • REQUIRED_FILES:测试需要的文件列表
  • ENVIRONMENT:设置环境变量
  • WORKING_DIRECTORY:设置工作目录

16.7 知识体系总览

下面这张图把CMake测试的整个流程串起来了,从启用到注册再到运行,一目了然:

CMake测试支持:从配置到运行 enable_testing() 在顶级CMakeLists.txt中调用 add_test() 注册测试用例名称与命令 cmake --build 编译测试可执行文件 ctest 运行所有注册的测试 编写测试用例 Google Test / Catch2 / assert 测试结果:通过 / 失败 / 超时 / 跳过 ctest 根据返回值判断,支持 -R -E -j 等选项 💡 核心流程:enable_testing() → add_test() → cmake --build → ctest

16.8 实际项目中的测试组织

最后聊聊我在真实项目里怎么组织测试的。一个中型C++项目,测试文件可能几十上百个,全堆在一个目录里肯定不行。

我一般这样搞:

project/
├── CMakeLists.txt          # enable_testing() 在这里
├── src/                    # 源码
├── tests/
│   ├── CMakeLists.txt      # 所有测试的构建和注册
│   ├── unit/               # 单元测试
│   │   ├── test_math.cpp
│   │   └── test_string.cpp
│   ├── integration/        # 集成测试
│   │   └── test_api.cpp
│   └── benchmark/          # 性能测试(可选)
│       └── test_sort.cpp

然后在 tests/CMakeLists.txt 里:

# 单元测试
add_subdirectory(unit)

# 集成测试
add_subdirectory(integration)

# 性能测试(通过选项控制)
option(ENABLE_BENCHMARK "Build benchmark tests" OFF)
if(ENABLE_BENCHMARK)
    add_subdirectory(benchmark)
endif()

每个子目录里的 CMakeLists.txt 负责构建和注册该模块的测试。这样层次清晰,新增测试时只需要加文件、改对应子目录的CMakeLists.txt,不会影响到其他地方。

个人经验: 我习惯给每个测试用例起一个有意义的名称,比如 MathTest.AddPositiveStringTest.TrimWhitespace。这样用 ctest -R 过滤时特别方便,而且CI报告里一眼就能看出哪个模块出了问题。

好了,CMake的测试支持就聊到这里。从 enable_testing()add_test(),再到 ctest 的运行与过滤,这一套组合拳打下来,项目的质量就有了最基本的保障。别等到代码炸了才想起写测试——嗯,这话是我用几次通宵加班换来的教训。


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