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 上则用 fullintegration 做更严格的检查。

继承与覆盖:减少重复配置

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
      }
    }
  }
}

这里 unitslow 都继承了 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 知识体系 CMake Presets 体系 configurePresets buildPresets testPresets 核心配置:configurePreset / filter / execution / output 快速检查 (quick) 过滤慢测试,短超时 完整测试 (full) 全部用例,较长超时 集成测试 (integration) 环境变量,长超时 继承机制 (inherits):基类 Preset → 子类 Preset 覆盖

避坑指南

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

  • 版本号要匹配:testPresets 的 version 字段必须和 CMake 版本兼容。CMake 3.20 以下不支持 testPresets,3.23 以上才支持 inherits。我曾经在 3.18 上折腾了半天,最后发现是版本问题。
  • configurePreset 必须存在:如果 testPreset 引用的 configurePreset 没定义,ctest 会直接报错退出。建议在同一个 JSON 文件里定义所有 Preset,避免引用混乱。
  • 环境变量覆盖:testPresets 里设置的环境变量会覆盖系统环境变量。如果你在本地调试时需要不同的值,可以用 --env 参数临时覆盖。
  • 过滤规则是正则filter.include.nameexclude.name 支持正则表达式。比如 test_* 匹配所有以 test_ 开头的测试。注意不要写错通配符。

特别注意:testPresets 不会自动构建!你需要先运行 cmake --preset <name>cmake --build --preset <name>,确保构建产物是最新的。我建议在 CI 脚本里把构建和测试写成两个步骤,不要混在一起。

好了,关于 testPresets 的核心内容就这些。说白了,它就是给 CTest 加了一层「配置管理层」。你只要花半小时把测试 Preset 写好,以后团队里每个人跑测试都是同样的参数、同样的环境、同样的过滤规则。这才是真正的标准化。

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