CTest标签与分组:set_tests_properties、LABELS属性、按标签运行测试

测试多了以后,你会面临一个很现实的问题——怎么挑着跑?

我最早接触CTest时,项目里只有十几个测试,一条命令全跑完,挺爽的。后来测试堆到两三百个,每次跑要等十几分钟。更烦的是,我只想跑单元测试,结果集成测试、性能测试全跟着跑了一遍。嗯,这时候就需要标签和分组了。

为什么需要标签?

说白了,标签就是给测试打上的分类标记。你可以按模块分、按类型分、按优先级分。想跑哪一组,就指定哪个标签。

我在一个嵌入式项目里遇到过这种情况:硬件相关的测试只能在开发板上跑,纯逻辑的测试在CI上就能跑。如果没有标签,每次都得手动挑测试,或者写一堆if判断。后来我用LABELS一标,世界清净了。

核心思路:用set_tests_properties给测试打标签,用ctest -L按标签筛选运行。

set_tests_properties 基础用法

这个命令可以修改测试的各种属性,LABELS只是其中之一。我们先看个最简单的例子:

add_test(NAME test_math COMMAND test_math)
add_test(NAME test_io COMMAND test_io)
add_test(NAME test_network COMMAND test_network)

set_tests_properties(test_math PROPERTIES LABELS "unit")
set_tests_properties(test_io PROPERTIES LABELS "unit")
set_tests_properties(test_network PROPERTIES LABELS "integration")

这样一标,test_math和test_io属于"unit"组,test_network属于"integration"组。

按标签运行测试

运行的时候,用-L参数指定标签:

# 只跑单元测试
ctest -L unit

# 只跑集成测试
ctest -L integration

# 排除某个标签(用 ! 前缀)
ctest -L "!slow"

注意,-L是正则匹配。如果你标签叫"unit",-L unit能匹配到,-L "unit.*"也能。我个人习惯用精确匹配,避免误伤。

小技巧:多个标签可以用|分隔,比如ctest -L "unit|integration",这样两个组的测试都会跑。

给测试打多个标签

一个测试可以属于多个分组。比如某个测试既是单元测试,又比较慢:

add_test(NAME test_big_matrix COMMAND test_big_matrix)
set_tests_properties(test_big_matrix PROPERTIES LABELS "unit;slow")

多个标签用分号分隔。这样:

  • ctest -L unit 会包含它
  • ctest -L slow 也会包含它
  • ctest -L "unit;slow" 要求同时匹配两个标签(注意这是AND逻辑)

注意:CTest的标签匹配默认是OR逻辑。如果你想用AND逻辑,需要写-L "unit&slow"。这个语法有点反直觉,我第一次用的时候就踩坑了。

批量设置标签

如果你有一堆测试要打同一个标签,一个个写太累了。可以用set_tests_properties一次搞定:

# 假设有 test_a, test_b, test_c 三个测试
set_tests_properties(
    test_a test_b test_c
    PROPERTIES LABELS "unit"
)

或者用CMake的列表操作:

set(UNIT_TESTS
    test_math
    test_string
    test_sort
    test_search
)

foreach(test_name ${UNIT_TESTS})
    add_test(NAME ${test_name} COMMAND ${test_name})
    set_tests_properties(${test_name} PROPERTIES LABELS "unit")
endforeach()

我个人更喜欢第二种方式,因为测试列表清晰,后期维护方便。

标签命名规范

标签怎么起名?我建议遵循几个原则:

维度 示例标签 说明
按类型 unit, integration, e2e, performance 测试的性质
按模块 module_io, module_net, module_gui 测试所属模块
按速度 fast, slow, nightly 测试耗时
按平台 linux, windows, embedded 测试运行平台
按优先级 p0, p1, p2 测试重要性

我曾经在一个项目里见过有人用中文标签,比如"单元测试"、"集成测试"。虽然CTest支持UTF-8,但命令行里切输入法很痛苦。我建议还是用英文小写加下划线,通用性最好。

实际项目中的标签策略

拿我之前参与的一个中间件项目举例,我们的标签体系是这样的:

# CMakeLists.txt 片段
add_test(NAME test_ring_buffer COMMAND test_ring_buffer)
set_tests_properties(test_ring_buffer PROPERTIES 
    LABELS "unit;fast;module_core"
)

add_test(NAME test_tcp_reconnect COMMAND test_tcp_reconnect)
set_tests_properties(test_tcp_reconnect PROPERTIES 
    LABELS "integration;slow;module_net"
)

add_test(NAME test_gui_render COMMAND test_gui_render)
set_tests_properties(test_gui_render PROPERTIES 
    LABELS "e2e;slow;module_gui;nightly"
)

这样,CI流水线里可以这样配置:

  • 每次提交:ctest -L "fast" —— 只跑快速测试,保证基本功能
  • 每日构建:ctest -L "nightly" —— 跑所有耗时测试
  • 发布前:ctest -L "unit|integration|e2e" —— 全量测试

经验之谈:标签不是越多越好。我见过有人给每个测试打了七八个标签,结果维护起来比写测试还累。一般来说,2-3个维度就够用了。

标签与CTest的其他属性配合

LABELS不是set_tests_properties能设置的唯一属性。常用的还有:

set_tests_properties(test_foo PROPERTIES
    LABELS "unit"           # 标签
    TIMEOUT 30              # 超时时间(秒)
    COST 2.5                # 测试成本(影响并行调度)
    REQUIRED_FILES "data.txt"  # 依赖文件
    ENVIRONMENT "VAR=value"    # 环境变量
    FIXTURES_REQUIRED "db"     # 依赖的fixture
)

标签和TIMEOUT配合使用效果很好。比如给"slow"标签的测试设置更长的超时时间:

set_tests_properties(test_heavy PROPERTIES 
    LABELS "slow"
    TIMEOUT 120
)

用标签做测试分组管理

除了命令行筛选,标签还可以在CTestTestfile.cmake里做更精细的控制。比如你想把某个目录下的所有测试都打上同一个标签:

# 在子目录的 CTestTestfile.cmake 中
add_test(NAME test_sub_a COMMAND test_sub_a)
add_test(NAME test_sub_b COMMAND test_sub_b)

# 批量设置标签
get_property(tests DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY TESTS)
foreach(test_name ${tests})
    set_tests_properties(${test_name} PROPERTIES LABELS "sub_module")
endforeach()

这个方法在大型项目中特别有用。你不需要在每个add_test后面都跟一个set_tests_properties,而是可以在文件末尾统一处理。

标签的调试技巧

有时候你忘了某个测试打了什么标签,可以用ctest -N配合--show-only查看:

# 列出所有测试及其标签
ctest -N --show-only=json-v1 | jq '.tests[] | {name: .name, labels: .properties.LABELS}'

如果没有jq,也可以用:

ctest -N --show-only=json-v1 > tests.json

然后打开JSON文件查看。嗯,这个方法虽然土,但管用。

快速检查:运行ctest -L "nonexistent_label",如果返回0个测试,说明你的标签写错了或者没匹配到。

SVG:CTest标签与分组核心逻辑

CTest标签与分组核心逻辑 测试用例(add_test) test_math test_io test_network test_gui 标签分配(set_tests_properties LABELS) unit; fast unit; slow integration; slow e2e; nightly 按标签筛选运行(ctest -L 标签名) 只运行匹配标签的测试

常见问题与避坑

问题1:标签名大小写敏感吗?

是的。LABELS属性是大小写敏感的。"Unit"和"unit"是两个不同的标签。我建议统一用小写,省得记混。

问题2:标签里能包含空格吗?

可以,但需要引号包裹。比如LABELS "my unit test"。不过我个人不建议这么做,命令行里处理空格很麻烦。

问题3:ctest -L 匹配不到测试?

我曾经遇到过这种情况:明明打了标签,但ctest -L xxx就是找不到。后来发现是CMake缓存问题。执行cmake --build .重新生成一下就好了。

避坑指南:如果你在子目录的CMakeLists.txt里用set_tests_properties,要确保add_test已经执行过了。CMake是声明式的,顺序很重要。我曾经把set_tests_properties写在add_test前面,结果标签根本没生效。

总结

标签这个东西,说白了就是给测试加个分类。用好了,你的测试管理会非常灵活。用不好,也就是多写几行代码的事。

我个人建议,从项目一开始就规划好标签体系。别等到测试堆到几百个了再回头补标签,那时候你连哪些测试该打什么标签都记不清了。

嗯,标签就聊到这里。记住三个核心命令:set_tests_properties打标签,ctest -L按标签跑,ctest -N --show-only查标签。够用了。


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