27、与 CTest 集成:通过 testPresets 自动化测试流程
说实话,很多团队把 CMake Presets 只当成编译配置的「快捷方式」,这有点浪费了。我个人觉得,testPresets 才是 Presets 体系里被低估的宝藏。你想想看,构建搞标准化了,测试流程却还是靠口头传命令:「你跑一下那个 test,对,就是那个带参数的...」——这不就脱节了吗?
testPresets 就是来解决这个问题的。它让你把测试的配置、参数、环境变量、过滤规则,全部写进 JSON 文件里。然后团队成员只需要敲一行 ctest --preset <name>,就能复现完全一致的测试环境。
为什么需要 testPresets?
我在项目里遇到过好几次这样的场景:新同事跑测试,发现一堆失败。我过去一看,好家伙,他用的测试参数跟 CI 上完全不一样。有些测试需要特定的环境变量,有些需要跳过某些耗时用例,这些全靠口头传递,迟早要出问题。
testPresets 的核心价值就三点:
- 可复现:测试配置与代码一起版本管理,不会丢失
- 可共享:团队所有人用同一套测试参数,减少「在我机器上能过」的尴尬
- 可分层:从单元测试到集成测试,不同 Preset 覆盖不同场景
核心思想:把测试的「怎么跑」也纳入标准化管理,而不仅仅是「怎么编译」。
testPresets 的基本结构
先看一个最简单的例子。假设你有一个项目,测试用例分布在不同的子目录里:
{
"version": 6,
"testPresets": {
"default": {
"name": "default",
"configurePreset": "default",
"output": {
"outputOnFailure": true,
"maxFailedTestOutputSize": 10240
}
}
}
}
这里 configurePreset 指定了依赖哪个构建配置。为什么要有这个字段?因为测试必须基于某个构建产物来跑。你总不能拿 Debug 的二进制去跑 Release 的测试吧?
常用配置项详解
testPresets 的配置项其实不少,但常用的就那么几个。我按使用频率排个序:
| 配置项 | 作用 | 我的建议 |
|---|---|---|
configurePreset |
关联的构建 Preset | 必填,否则不知道用哪个构建产物 |
output.outputOnFailure |
失败时输出详细日志 | 建议开启,省得排查时再跑一遍 |
filter.include/exclude |
按名称过滤测试用例 | 常用,比如只跑某个模块的测试 |
environment |
设置环境变量 | 集成测试必备,比如数据库连接串 |
execution.timeout |
单个测试超时时间 | 防止死循环测试卡住整个流程 |
execution.jobs |
并行测试数 | 根据机器核心数调整,别设太大 |
实战:分层测试配置
我习惯把测试分成三层:快速检查、完整测试、集成测试。每一层对应一个 testPreset:
{
"version": 6,
"testPresets": {
"quick": {
"name": "quick",
"configurePreset": "debug",
"filter": {
"include": {
"name": "test_*"
},
"exclude": {
"name": "*_integration_*"
}
},
"execution": {
"jobs": 4,
"timeout": 30
},
"output": {
"outputOnFailure": true
}
},
"full": {
"name": "full",
"configurePreset": "release",
"execution": {
"jobs": 8,
"timeout": 120
},
"output": {
"outputOnFailure": true,
"maxFailedTestOutputSize": 65536
}
},
"integration": {
"name": "integration",
"configurePreset": "release",
"filter": {
"include": {
"name": "*_integration_*"
}
},
"environment": {
"DB_HOST": "localhost",
"DB_PORT": "5432",
"TEST_MODE": "integration"
},
"execution": {
"timeout": 300
}
}
}
}
你看,quick 只跑单元测试,排除集成测试,超时短,适合开发阶段频繁使用。full 跑所有测试,超时更长,适合提交前验证。integration 专门跑集成测试,需要设置数据库环境变量。
小技巧:我一般把 quick 作为默认 Preset,这样团队成员敲 ctest --preset default 就能快速验证。CI 上则用 full 和 integration 做更严格的检查。
继承与覆盖:减少重复配置
testPresets 也支持 inherits 机制。如果你有多个 Preset 共享大部分配置,可以提取一个基类:
{
"version": 6,
"testPresets": {
"base": {
"name": "base",
"configurePreset": "release",
"output": {
"outputOnFailure": true,
"maxFailedTestOutputSize": 4096
},
"execution": {
"jobs": 4
}
},
"unit": {
"name": "unit",
"inherits": "base",
"filter": {
"exclude": {
"name": "*_slow_*"
}
}
},
"slow": {
"name": "slow",
"inherits": "base",
"filter": {
"include": {
"name": "*_slow_*"
}
},
"execution": {
"timeout": 600
}
}
}
}
这里 unit 和 slow 都继承了 base 的公共配置,然后各自覆盖了过滤规则和超时时间。嗯,这样写起来清爽多了。
与 CTest 命令行配合
testPresets 最终还是要靠 ctest 命令来执行。常用的命令组合:
# 列出所有可用的 testPreset
ctest --list-presets
# 使用指定 Preset 运行测试
ctest --preset quick
# 覆盖 Preset 中的某个参数(比如临时增加并行数)
ctest --preset full --parallel 16
# 查看 Preset 展开后的完整配置
ctest --preset integration --debug
我曾经遇到过一个问题:团队成员不知道有哪些 Preset 可用。后来我在 CI 脚本里加了一步 ctest --list-presets 的输出,大家一看就明白了。说白了,可见性很重要。
知识体系图
下面这张图展示了 testPresets 在整个 CMake Presets 体系中的位置,以及它内部的核心逻辑:
避坑指南
最后分享几个我踩过的坑:
- 版本号要匹配:testPresets 的
version字段必须和 CMake 版本兼容。CMake 3.20 以下不支持 testPresets,3.23 以上才支持inherits。我曾经在 3.18 上折腾了半天,最后发现是版本问题。 - configurePreset 必须存在:如果 testPreset 引用的 configurePreset 没定义,ctest 会直接报错退出。建议在同一个 JSON 文件里定义所有 Preset,避免引用混乱。
- 环境变量覆盖:testPresets 里设置的环境变量会覆盖系统环境变量。如果你在本地调试时需要不同的值,可以用
--env参数临时覆盖。 - 过滤规则是正则:
filter.include.name和exclude.name支持正则表达式。比如test_*匹配所有以test_开头的测试。注意不要写错通配符。
特别注意:testPresets 不会自动构建!你需要先运行 cmake --preset <name> 和 cmake --build --preset <name>,确保构建产物是最新的。我建议在 CI 脚本里把构建和测试写成两个步骤,不要混在一起。
好了,关于 testPresets 的核心内容就这些。说白了,它就是给 CTest 加了一层「配置管理层」。你只要花半小时把测试 Preset 写好,以后团队里每个人跑测试都是同样的参数、同样的环境、同样的过滤规则。这才是真正的标准化。