第32章:测试与CTest:enable_testing()和add_test(),运行单元测试

说实话,很多C++项目写完了,编译通过,跑起来也没报错,但你真的敢说它没问题吗?
我以前也不敢。直到我把CTest用起来,才觉得心里踏实多了。

这一章,我们就聊聊CMake内置的测试工具——CTest。
说白了,它帮你把测试用例组织起来,一键运行,还能看结果汇总。

32.1 为什么需要CTest?

我见过不少项目,测试代码写了一大堆,但没人跑。
为什么?因为没有统一的入口。
有人用gtest直接编译运行,有人写了个shell脚本,还有人干脆把测试代码注释掉了。

CTest解决的就是这个问题:
它让所有测试用例,都能通过 make testctest 命令统一执行。
你不需要记住每个测试的可执行文件叫什么,也不需要手动一个个点开。

核心价值: 统一的测试入口 + 自动化的测试执行 + 清晰的测试报告。

32.2 启用测试:enable_testing()

在CMake中启用测试,只需要一行:

enable_testing()

这行代码必须写在顶级 CMakeLists.txt 中。
为什么?因为CTest需要从顶层开始递归查找测试用例。
如果你写在子目录里,CTest可能找不到。

注意: enable_testing() 必须在 add_subdirectory() 之前调用,否则子目录中的测试不会被注册。

我个人习惯把它放在项目最开始,紧挨着 project() 之后:

cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0)
enable_testing()

add_subdirectory(src)
add_subdirectory(tests)

32.3 添加测试:add_test()

有了测试框架,接下来就是添加具体的测试用例了。
add_test() 的语法很简单:

add_test(NAME <test_name> COMMAND <executable> [args...])

举个例子:

add_test(NAME test_math COMMAND test_math_exe)

这里 test_math_exe 是一个可执行文件,它内部包含了测试逻辑。
CTest会运行它,然后根据返回值判断是否通过:返回0表示通过,非0表示失败。

小技巧: 你可以给测试传参数。比如 add_test(NAME test_math COMMAND test_math_exe --gtest_filter="Add*"),只运行名字以Add开头的测试。

32.4 一个完整的例子

我们来看一个实际项目中的用法。
假设项目结构如下:

project/
├── CMakeLists.txt
├── src/
│   ├── CMakeLists.txt
│   └── math.cpp
└── tests/
    ├── CMakeLists.txt
    └── test_math.cpp

顶级 CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(MathLib VERSION 1.0)
enable_testing()

add_subdirectory(src)
add_subdirectory(tests)

tests/CMakeLists.txt:

add_executable(test_math_exe test_math.cpp)
target_link_libraries(test_math_exe PRIVATE MathLib)

add_test(NAME test_math_add COMMAND test_math_exe --test=add)
add_test(NAME test_math_sub COMMAND test_math_exe --test=sub)

这样,当你运行 make testctest 时,CTest会自动执行这两个测试用例。

32.5 测试框架的选择

CTest本身不写测试代码,它只负责运行。
测试代码怎么写?你可以用任何框架:

框架 特点 我是否推荐
Google Test 功能强大,社区活跃 强烈推荐
Catch2 单头文件,轻量 适合小项目
Boost.Test 功能全面,但依赖Boost 看情况
自定义main函数 简单粗暴 不推荐,除非你很清楚自己在做什么

我个人最常用Google Test。它在CMake中集成非常方便:

find_package(GTest REQUIRED)
add_executable(test_math_exe test_math.cpp)
target_link_libraries(test_math_exe PRIVATE GTest::GTest GTest::Main)
add_test(NAME test_math COMMAND test_math_exe)

32.6 运行测试的几种方式

配置好之后,运行测试有三种常见方式:

  1. make test:最直观,但需要先make编译。
  2. ctest:直接运行,CTest会自动编译并执行测试。
  3. ctest -V:显示详细输出,适合调试。

我记得有一次,测试一直失败,但看不到具体错误信息。
后来发现是忘了加 -V 参数。加上之后,错误日志一目了然。

避坑指南: 如果你用 ctest 发现测试没跑,先检查 enable_testing() 是否在顶级CMakeLists.txt中,并且是否在 add_subdirectory() 之前。

32.7 测试的组织与命名规范

项目大了,测试用例会很多。
我建议你给测试命名时,遵循一定的规范:

  • 模块名_功能名:比如 math_addmath_sub
  • 测试名要能反映测试内容:比如 test_add_positive_numbers
  • 用下划线分隔:避免空格带来的麻烦

我曾经接手过一个项目,测试命名乱七八糟,有叫 test1 的,有叫 check 的。
结果想跳过某个测试时,根本不知道哪个是哪个。
后来我花了一整天重新整理命名,才把测试管理起来。

32.8 测试的过滤与分组

CTest支持通过标签(LABELS)对测试进行分组:

add_test(NAME test_math_add COMMAND test_math_exe)
set_tests_properties(test_math_add PROPERTIES LABELS "math;core")

然后你可以只运行某个标签的测试:

ctest -L math

或者排除某个标签:

ctest -LE slow

这个功能在大型项目中特别有用。
比如你只改了数学模块,就可以只跑 math 标签的测试,节省大量时间。

32.9 测试的超时与资源管理

有些测试可能跑得很慢,或者陷入死循环。
CTest提供了超时机制:

set_tests_properties(test_math_add PROPERTIES TIMEOUT 10)

超过10秒,CTest会强制终止这个测试,并标记为失败。

另外,如果测试需要特定环境变量,可以用 ENVIRONMENT 属性:

set_tests_properties(test_math_add PROPERTIES ENVIRONMENT "MY_VAR=hello")

32.10 知识体系图

下面这张图总结了CTest的核心流程:

CTest 测试流程 enable_testing() 在顶级CMakeLists.txt中启用 add_test() 注册测试用例 make test / ctest 执行测试 测试框架(GTest等) 测试用例(多个) 测试结果 set_tests_properties() 设置标签/超时/环境变量

32.11 避坑总结

最后,我把这些年踩过的坑总结一下:

  • enable_testing() 位置不对:必须在顶级CMakeLists.txt中,且在add_subdirectory之前。
  • 测试可执行文件没链接对库:测试代码需要链接被测试的库,否则编译通过但运行崩溃。
  • 忘记加 -V 参数:看不到错误详情,排查问题很痛苦。
  • 测试命名冲突:不同模块的测试同名,CTest会覆盖前面的。
  • 测试超时未设置:死循环的测试会卡住整个CI流程。

嗯,CTest其实不复杂,但用好了,能让你的项目质量上一个台阶。
从今天开始,给你的项目加上测试吧。


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