17、CMake中的CTest高级用法:测试标签、测试超时、资源锁定、并行测试、测试覆盖率

CTest 这东西,很多人以为就是个跑测试的工具。嗯,确实它能跑测试,但它的能力远不止这些。我在几个大型项目中折腾过 CTest,发现用好它的高级功能,能让你的测试流程脱胎换骨。今天咱们就聊聊这些硬核玩法。

测试标签:给测试分门别类

项目大了以后,测试用例会越来越多。几百个甚至上千个测试,每次全跑一遍?太浪费时间了。这时候就需要给测试打标签。

打个比方,你有个项目,包含单元测试、集成测试、性能测试。你不可能每次提交代码都跑性能测试吧?那玩意儿跑一次半小时。所以,给测试打上标签,按需运行,这才是正道。

在 CMake 里,用 add_test 配合 set_tests_properties 就能搞定:

add_test(NAME test_math COMMAND test_math)
set_tests_properties(test_math PROPERTIES LABELS "unit")

add_test(NAME test_db COMMAND test_db)
set_tests_properties(test_db PROPERTIES LABELS "integration")

add_test(NAME test_perf COMMAND test_perf)
set_tests_properties(test_perf PROPERTIES LABELS "performance")

跑测试的时候,用 -L 参数指定标签:

ctest -L unit          # 只跑单元测试
ctest -L integration   # 只跑集成测试
ctest -L performance   # 只跑性能测试
ctest -L "unit|integration"  # 跑单元和集成测试

我个人习惯把标签分成三级:quicknormalfull。quick 是几秒钟就能跑完的,normal 是几分钟的,full 是那种要跑半小时以上的。这样在 CI 里可以灵活控制。

小技巧: 标签名尽量用英文小写,别用空格。我见过有人用 "unit test" 做标签,结果命令行里还得加引号,麻烦得很。

测试超时:别让测试卡死你

你有没有遇到过这种情况?跑测试的时候,某个测试用例卡住了,整个测试流程就停在那里。我等过最久的一次,是一个网络相关的测试,等了 20 分钟才发现它死锁了。

CTest 提供了超时机制,可以给每个测试设置最大运行时间。超时了就直接杀掉,标记为失败。

set_tests_properties(test_network 
    PROPERTIES TIMEOUT 30)  # 30秒超时

也可以全局设置默认超时:

ctest --timeout 60  # 所有测试默认60秒超时

我曾经在一个项目中,把所有测试都加了超时。效果立竿见影——CI 再也不会因为某个测试卡住而挂掉一整天了。

注意: 超时时间别设太短。有些测试在负载高的机器上跑得慢,设太短会导致误报。我一般给单元测试设 30 秒,集成测试设 120 秒。

资源锁定:避免测试互相干扰

并行测试有个头疼的问题——资源冲突。比如两个测试都要用同一个数据库,或者都要监听同一个端口。同时跑的话,肯定有一个会失败。

CTest 提供了资源锁定功能,可以给测试分配资源,让它们互斥执行。

先定义资源:

set(CTEST_RESOURCE_SPEC_FILE "${CMAKE_CURRENT_SOURCE_DIR}/resource_spec.json")

resource_spec.json 文件内容:

{
  "database": {
    "count": 1
  },
  "port_8080": {
    "count": 1
  }
}

然后给测试绑定资源:

set_tests_properties(test_db1 PROPERTIES 
    RESOURCE_GROUPS "database")
set_tests_properties(test_db2 PROPERTIES 
    RESOURCE_GROUPS "database")

这样,test_db1 和 test_db2 就不会同时运行了。它们会排队,一个跑完另一个再跑。

嗯,这里要注意,资源锁定需要 CTest 的并行调度器支持。如果你用 ctest -j4,它会自动处理资源分配。

并行测试:让测试飞起来

现在的机器都是多核的,不利用起来太浪费了。CTest 的并行测试很简单:

ctest -j4   # 4个测试同时跑
ctest -j8   # 8个测试同时跑

但并行测试不是越多越好。我试过在 8 核机器上开 16 个并行测试,结果 CPU 跑满了,每个测试反而更慢。为什么呢?因为测试之间会抢 CPU、抢内存、抢 IO。

我个人建议,并行数设为 CPU 核心数的 1.5 倍左右。比如 4 核机器用 -j6,8 核机器用 -j12。这样既能充分利用资源,又不会过度竞争。

另外,并行测试时,输出会混在一起。CTest 提供了 --output-on-failure 参数,只输出失败测试的详细信息:

ctest -j4 --output-on-failure

这样跑完一看,哪些测试失败了,它们的输出是什么,一目了然。

测试覆盖率:看看你的测试到底测了多少

测试覆盖率是个好东西,但也是个坑。好东西在于,它能告诉你哪些代码没被测试到。坑在于,很多人追求 100% 覆盖率,结果写了一堆没意义的测试。

CTest 本身不直接做覆盖率分析,但它可以和 gcov/lcov 配合,生成覆盖率报告。

首先,编译时需要开启覆盖率支持:

cmake -DCMAKE_CXX_FLAGS="--coverage" ..
cmake --build .

然后跑测试:

ctest -j4

最后生成报告:

lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage_report

打开 coverage_report/index.html,就能看到详细的覆盖率报告了。

我在项目中遇到过一个问题:覆盖率报告里总是包含第三方库的代码。解决办法是用 --exclude 参数过滤掉:

lcov --capture --directory . --output-file coverage.info \
     --exclude '/usr/*' --exclude '*/third_party/*'

这样报告就只显示你自己的代码了。

核心观点: 覆盖率不是越高越好。我见过有人为了 100% 覆盖率,写测试去测 getter/setter,纯属浪费生命。重点测业务逻辑、边界条件、异常路径。80% 的覆盖率,测到关键路径,比 100% 的垃圾测试有用得多。

知识体系总览

下面这张图,把 CTest 高级功能的核心逻辑串起来了:

CTest 高级功能 测试标签 测试超时 资源锁定 并行测试 测试覆盖率 核心逻辑 标签分类 → 超时保护 → 资源隔离 → 并行加速 → 覆盖率验证 环环相扣,构建可靠的自动化测试体系

这五个功能,说白了就是让你的测试更智能、更可靠、更高效。标签让你按需跑测试,超时防止测试卡死,资源锁定避免冲突,并行测试加速,覆盖率帮你评估测试质量。

我在实际项目中,把这五个功能组合起来用。比如 CI 流程里,先跑带 quick 标签的测试,超时设 30 秒,并行 4 个。如果都通过了,再跑 normal 标签的,超时设 120 秒,并行 2 个。最后跑覆盖率,生成报告。这样既快又稳。

好了,CTest 的高级用法就聊到这儿。这些功能用好了,你的测试流程会变得非常顺手。下次咱们聊聊 CPack 的打包技巧,那个也很有意思。


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