20、测试:enable_testing(),add_test(),使用CTest运行测试,测试超时与标签

测试这件事,说实话,在C++项目里经常被忽视。我见过太多项目,代码写得飞起,一跑测试就崩。嗯,我自己也踩过这个坑。今天我们就聊聊CMake里怎么把测试搞起来。

enable_testing():开启测试能力

这个函数很简单,但位置很关键。你必须在顶层的CMakeLists.txt里调用它,而且要在project()之后。

cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0)

enable_testing()  # 开启测试支持

add_subdirectory(src)
add_subdirectory(tests)

为什么必须放在顶层?因为CTest需要知道整个项目的测试结构。我曾经试过在子目录里调用enable_testing(),结果CTest根本找不到测试。说白了,这就是个全局开关。

注意:enable_testing() 必须在 add_subdirectory() 之前调用,否则子目录里的 add_test() 会失效。

add_test():注册测试用例

有了测试框架,接下来就是注册测试。最基础的方式是这样的:

add_test(NAME test_math COMMAND test_math_executable)

这里NAME是测试的名字,COMMAND后面跟的是可执行文件。你想想看,如果测试可执行文件需要参数怎么办?很简单:

add_test(NAME test_add COMMAND test_math_executable --add 2 3)
add_test(NAME test_sub COMMAND test_math_executable --sub 5 2)

我个人习惯把测试可执行文件统一放在tests/目录下,然后用add_test一个个注册。这样结构清晰,找问题也方便。

使用CTest运行测试

测试注册好了,怎么跑?两种方式:

  1. 命令行直接跑:在构建目录里执行 ctest
  2. 通过CMake构建目标:执行 cmake --build . --target test

我个人更推荐第一种,因为CTest的参数更丰富。比如:

ctest -V          # 显示详细输出
ctest -N          # 只列出测试,不执行
ctest -R math     # 只运行名字包含"math"的测试
ctest -E slow     # 排除名字包含"slow"的测试
小技巧:ctest --output-on-failure 可以让失败的测试输出详细信息,调试时特别有用。

测试超时:防止测试卡死

你有没有遇到过测试跑着跑着就卡住了?我有。有一次一个网络相关的测试,因为服务器没响应,硬是等了5分钟。从那以后,我养成了给测试加超时的习惯。

add_test(NAME test_network COMMAND test_network)
set_tests_properties(test_network PROPERTIES TIMEOUT 10)

这里设置了10秒超时。如果测试超过10秒还没结束,CTest会直接杀掉它,并标记为失败。

你也可以批量设置:

set_tests_properties(
  test_network test_database test_api
  PROPERTIES TIMEOUT 30
)
注意:超时时间别设太短。我见过有人设1秒,结果测试在CI环境里因为机器负载高频繁超时。建议根据测试的实际耗时,留出2-3倍的余量。

测试标签:给测试分类

项目大了以后,测试会越来越多。这时候标签就派上用场了。你可以给测试打上不同的标签,然后按标签筛选执行。

add_test(NAME test_unit_math COMMAND test_math)
set_tests_properties(test_unit_math PROPERTIES LABELS "unit;math")

add_test(NAME test_integration_db COMMAND test_db)
set_tests_properties(test_integration_db PROPERTIES LABELS "integration;database")

然后你可以这样运行:

ctest -L unit          # 只运行单元测试
ctest -L integration   # 只运行集成测试
ctest -L math          # 只运行数学相关的测试

标签用分号分隔,可以打多个。我个人习惯给每个测试至少打两个标签:一个表示测试类型(unit/integration/e2e),一个表示测试模块(math/db/api)。这样筛选起来特别灵活。

实战示例:完整的测试配置

来看一个完整的例子,把上面这些知识点串起来:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(Calculator VERSION 1.0)

enable_testing()

add_executable(calc main.cpp add.cpp sub.cpp)

# 测试目录
add_subdirectory(tests)
# tests/CMakeLists.txt
# 编译测试可执行文件
add_executable(test_add test_add.cpp)
target_link_libraries(test_add PRIVATE calc_lib)

add_executable(test_sub test_sub.cpp)
target_link_libraries(test_sub PRIVATE calc_lib)

# 注册测试
add_test(NAME test_add_positive COMMAND test_add 2 3)
add_test(NAME test_add_negative COMMAND test_add -1 -2)
add_test(NAME test_sub_positive COMMAND test_sub 5 2)
add_test(NAME test_sub_negative COMMAND test_sub -3 -1)

# 设置超时
set_tests_properties(
  test_add_positive test_add_negative
  test_sub_positive test_sub_negative
  PROPERTIES TIMEOUT 5
)

# 设置标签
set_tests_properties(test_add_positive test_add_negative PROPERTIES LABELS "unit;add")
set_tests_properties(test_sub_positive test_sub_negative PROPERTIES LABELS "unit;sub")

跑一下看看:

mkdir build && cd build
cmake ..
cmake --build .
ctest -V
核心要点:
  • enable_testing() 必须在顶层CMakeLists.txt中调用
  • add_test() 注册测试,NAME和COMMAND是必填参数
  • TIMEOUT属性防止测试卡死,建议留余量
  • LABELS属性给测试分类,方便筛选执行
  • ctest -V 显示详细输出,-L按标签筛选,-R按名字筛选

测试这件事,说白了就是给代码上保险。刚开始可能觉得麻烦,但等你被线上bug坑过几次,就知道测试有多重要了。嗯,今天就聊到这,去给你的项目加几个测试吧。

CMake测试体系结构图 enable_testing() add_test(NAME ... COMMAND ...) TIMEOUT 防止测试卡死 LABELS 给测试分类 WORKING_DIRECTORY 指定工作目录 CTest 运行测试 常用命令:ctest -V | ctest -L unit | ctest -R math
公众号:蓝海资料掘金营,微信 deep3321