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" # 跑单元和集成测试
我个人习惯把标签分成三级:quick、normal、full。quick 是几秒钟就能跑完的,normal 是几分钟的,full 是那种要跑半小时以上的。这样在 CI 里可以灵活控制。
测试超时:别让测试卡死你
你有没有遇到过这种情况?跑测试的时候,某个测试用例卡住了,整个测试流程就停在那里。我等过最久的一次,是一个网络相关的测试,等了 20 分钟才发现它死锁了。
CTest 提供了超时机制,可以给每个测试设置最大运行时间。超时了就直接杀掉,标记为失败。
set_tests_properties(test_network
PROPERTIES TIMEOUT 30) # 30秒超时
也可以全局设置默认超时:
ctest --timeout 60 # 所有测试默认60秒超时
我曾经在一个项目中,把所有测试都加了超时。效果立竿见影——CI 再也不会因为某个测试卡住而挂掉一整天了。
资源锁定:避免测试互相干扰
并行测试有个头疼的问题——资源冲突。比如两个测试都要用同一个数据库,或者都要监听同一个端口。同时跑的话,肯定有一个会失败。
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/*'
这样报告就只显示你自己的代码了。
知识体系总览
下面这张图,把 CTest 高级功能的核心逻辑串起来了:
这五个功能,说白了就是让你的测试更智能、更可靠、更高效。标签让你按需跑测试,超时防止测试卡死,资源锁定避免冲突,并行测试加速,覆盖率帮你评估测试质量。
我在实际项目中,把这五个功能组合起来用。比如 CI 流程里,先跑带 quick 标签的测试,超时设 30 秒,并行 4 个。如果都通过了,再跑 normal 标签的,超时设 120 秒,并行 2 个。最后跑覆盖率,生成报告。这样既快又稳。
好了,CTest 的高级用法就聊到这儿。这些功能用好了,你的测试流程会变得非常顺手。下次咱们聊聊 CPack 的打包技巧,那个也很有意思。