6、测试预设(testPresets):定义测试配置、过滤、并行度等。
说实话,很多人把CMake Presets只当成编译配置的快捷方式。其实测试预设(testPresets)才是真正能提升团队效率的利器。我个人的经验是,一个项目能不能快速迭代,很大程度上取决于测试跑得够不够快、够不够准。
测试预设,说白了就是帮你把CTest的各种参数固化下来。你不用每次都在命令行里敲一堆-j、-R、-E之类的参数。你想想看,团队里新来的同事,只要一个cmake --preset test就能跑对测试,多省心。
6.1 测试预设的基本结构
测试预设和构建预设长得有点像,但它的核心是控制CTest的行为。我们先看一个最简单的例子:
{
"version": 6,
"testPresets": [
{
"name": "default",
"displayName": "默认测试",
"description": "跑所有单元测试,4核并行",
"configurePreset": "default",
"output": {
"outputOnFailure": true,
"verbosity": "default"
},
"execution": {
"jobs": 4,
"stopOnFailure": false
}
}
]
}
这里有个关键点:configurePreset字段。它告诉CMake,这个测试预设是基于哪个构建预设的。我在项目中遇到过有人忘了配这个字段,结果测试跑的是旧版本的二进制文件,排查了半天才发现问题。嗯,这个坑大家要注意。
6.2 过滤测试:只跑你关心的
大型项目里测试用例动辄上千个,全跑一遍太费时间。测试预设提供了强大的过滤机制。我个人习惯把测试分成几个层级:单元测试、集成测试、冒烟测试。每个层级对应一个预设。
{
"testPresets": [
{
"name": "unit",
"displayName": "单元测试",
"configurePreset": "default",
"filter": {
"include": {
"name": "^test_unit_"
},
"exclude": {
"name": "slow"
}
}
},
{
"name": "smoke",
"displayName": "冒烟测试",
"configurePreset": "default",
"filter": {
"include": {
"name": "smoke"
}
},
"execution": {
"jobs": 1,
"timeout": 300
}
}
]
}
你看,filter里的include和exclude支持正则表达式。我曾经有个项目,测试命名不规范,有的叫test_xxx,有的叫xxx_test。后来我统一了命名规范,再用正则过滤就方便多了。
6.3 并行度与资源控制
并行度是测试预设里最实用的功能之一。你想想看,一个测试跑10分钟,如果单核跑,那得等到猴年马月。但并行度也不是越高越好,得看你的机器有多少核,以及测试之间有没有资源冲突。
{
"testPresets": [
{
"name": "parallel",
"displayName": "并行测试",
"configurePreset": "default",
"execution": {
"jobs": 8,
"resourceSpecFile": "${sourceDir}/resources.json",
"testLoad": 4.0
}
}
]
}
这里有个testLoad参数,它控制的是CPU负载阈值。当系统负载超过这个值时,CTest会暂停启动新的测试。我在服务器上跑CI时,一般设成4.0,防止测试把CPU占满导致其他服务卡顿。
resourceSpecFile是CMake Presets 6引入的新功能。你可以为每个测试指定它需要的资源(比如GPU、内存大小)。CTest会根据资源分配情况智能调度测试。举个例子:
{
"version": 6,
"resources": {
"gpu": {
"type": "GPU",
"count": 2
}
},
"tests": [
{
"name": "test_gpu_1",
"resources": {
"gpu": 1
}
},
{
"name": "test_gpu_2",
"resources": {
"gpu": 1
}
}
]
}
这样,即使你设了jobs: 8,CTest也不会同时跑两个需要GPU的测试,因为GPU资源只有2个。嗯,这个设计很贴心。
6.4 环境变量与输出控制
测试环境往往需要一些特殊的环境变量。比如数据库连接串、API密钥之类的。测试预设可以帮你管理这些:
{
"testPresets": [
{
"name": "integration",
"displayName": "集成测试",
"configurePreset": "default",
"environment": {
"DB_HOST": "localhost",
"DB_PORT": "5432",
"LOG_LEVEL": "debug"
},
"output": {
"outputOnFailure": true,
"maxFailures": 10,
"shortProgress": true
}
}
]
}
输出控制这块,outputOnFailure是我最常用的。只输出失败的测试日志,成功的一概不显示。这样CI日志看起来清爽多了。maxFailures也很实用,一旦失败数达到阈值,CTest就停止跑剩下的测试,节省时间。
${sourceDir}/.env之类的文件加载,或者用CI平台的密钥管理功能。
6.5 测试预设的继承与覆盖
和构建预设一样,测试预设也支持继承。你可以定义一个基础预设,然后派生出不同场景的预设:
{
"testPresets": [
{
"name": "base",
"displayName": "基础测试配置",
"configurePreset": "default",
"execution": {
"jobs": 4,
"stopOnFailure": false
},
"output": {
"outputOnFailure": true
}
},
{
"name": "ci",
"displayName": "CI环境测试",
"inherits": "base",
"execution": {
"jobs": 8,
"stopOnFailure": true
},
"filter": {
"exclude": {
"label": "flaky"
}
}
},
{
"name": "dev",
"displayName": "开发环境测试",
"inherits": "base",
"filter": {
"include": {
"name": "quick"
}
}
}
]
}
你看,ci预设继承了base,但把并行度从4改成了8,并且开启了失败即停止。而dev预设只跑名字带"quick"的测试。这样团队里不同角色的人,都能用最适合自己的配置。
我曾经在团队里推广这个模式,一开始有人觉得麻烦。后来我给他们演示了一下:开发时用dev预设,几秒钟就跑完;提交前用ci预设,确保所有关键测试通过。大家用了都说好。
6.6 知识体系图
下面这张图总结了测试预设的核心逻辑:
6.7 实战建议
最后,我分享几个实战中的小建议:
- 命名规范:测试预设的名字最好和构建预设对应起来,比如
debug-test、release-test。这样一看就知道是哪个构建环境下的测试。 - 不要滥用继承:继承层级太深会让人晕头转向。我一般控制在两层以内:一个基础预设,然后派生几个具体场景的预设。
- 善用标签过滤:在测试代码里给测试打上标签(label),比如
unit、integration、slow。然后在预设里用filter.label来筛选,比用正则匹配名字更灵活。 - 本地测试和CI测试分开:本地测试追求速度,CI测试追求覆盖率。用不同的预设来管理,互不干扰。
核心要点: 测试预设不是简单的CTest参数封装,它是团队测试策略的体现。好的测试预设配置,能让每个人在合适的场景下跑合适的测试,既快又准。
好了,关于测试预设就聊到这里。你可以在自己的项目里试试看,从最简单的并行度配置开始,慢慢加上过滤、环境变量、继承。相信我,用上之后你就回不去了。