23、CMake测试:enable_testing、add_test、ctest、GoogleTest集成
测试这件事,说实话,很多嵌入式工程师一开始都不太重视。我早年做项目时也是这样——总觉得「代码能跑就行」,测试嘛,手动点两下就完了。直到有一次,一个底层驱动的小改动,把整个协议栈搞崩了,排查了整整两天。嗯,从那以后,我再也不敢轻视自动化测试了。
CMake 对测试的支持其实很成熟。你不需要额外装什么复杂的框架,CMake 自带的 enable_testing 和 add_test 就能搭起一套基础的测试体系。如果你想要更专业的测试能力,GoogleTest 也是无缝集成的。今天我就带你把这套东西捋清楚。
23.1 enable_testing:开启测试开关
说白了,enable_testing() 就是告诉 CMake:「嘿,我这个项目要做测试了,你准备一下。」这个命令必须在顶层的 CMakeLists.txt 中调用,而且只能调用一次。
关键点: enable_testing() 必须在 project() 之后、add_test() 之前调用。顺序错了,测试系统就起不来。
我个人习惯把它放在项目配置的早期,紧跟在 project() 后面。这样整个项目的子目录都能共享这个测试环境。
cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0)
enable_testing()
add_subdirectory(src)
add_subdirectory(test)
23.2 add_test:注册你的测试用例
add_test() 是用来注册测试用例的。它的基本用法很简单:
add_test(NAME test_name COMMAND executable [args...])
这里的 executable 可以是你自己编译出来的测试程序,也可以是系统命令。比如:
add_test(NAME test_math COMMAND test_math_app)
add_test(NAME test_memory COMMAND test_memory_app --verbose)
我在项目中遇到过一个问题:测试程序依赖某些环境变量,但 add_test 默认不会继承 shell 的环境。后来我用了 set_tests_properties 来设置环境变量:
set_tests_properties(test_memory PROPERTIES
ENVIRONMENT "MEMORY_DEBUG=1;LOG_LEVEL=info"
)
小技巧: 你可以用 --output-on-failure 参数让 ctest 在测试失败时打印输出,调试起来方便很多。
23.3 ctest:运行测试的指挥官
测试注册好了,怎么跑?用 ctest。它是 CMake 自带的测试驱动工具。你不需要写任何额外的脚本,直接在构建目录下执行:
mkdir build && cd build
cmake ..
cmake --build .
ctest
ctest 会找到所有通过 add_test 注册的测试,逐个运行并报告结果。常用的参数我列一下:
| 参数 | 作用 |
|---|---|
-V / --verbose |
显示详细输出 |
-N |
只列出测试,不运行 |
-R |
运行匹配正则的测试 |
-E |
排除匹配正则的测试 |
-j |
并行运行 N 个测试 |
--output-on-failure |
失败时输出日志 |
举个例子,我只想跑名字里带 math 的测试:
ctest -R math -V
我曾经在一个大型项目中,测试用例有 2000 多个。用 ctest -j 8 并行跑,从原来的 20 分钟缩短到 3 分钟。你想想看,这效率提升多明显。
23.4 GoogleTest 集成:专业级测试框架
如果你觉得 add_test 太基础,想要更丰富的断言、测试夹具、参数化测试,那 GoogleTest 就是你的菜。CMake 对 GoogleTest 的支持非常友好,尤其是从 CMake 3.11 开始,引入了 GTest 模块。
集成步骤其实就三步:
- 下载或引入 GoogleTest:推荐用
FetchContent或add_subdirectory - 编写测试代码:用
TEST()宏定义测试用例 - 链接并注册:用
gtest_discover_tests()自动发现测试
来看一个完整的例子:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(MyProject)
enable_testing()
# 引入 GoogleTest
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/release-1.12.1.tar.gz
)
FetchContent_MakeAvailable(googletest)
# 编译测试程序
add_executable(test_math test_math.cpp)
target_link_libraries(test_math GTest::GTest GTest::Main)
# 自动发现并注册测试
include(GoogleTest)
gtest_discover_tests(test_math)
对应的测试代码 test_math.cpp:
#include <gtest/gtest.h>
int add(int a, int b) { return a + b; }
TEST(MathTest, AddPositive) {
EXPECT_EQ(add(1, 2), 3);
EXPECT_EQ(add(10, 20), 30);
}
TEST(MathTest, AddNegative) {
EXPECT_EQ(add(-1, -2), -3);
EXPECT_EQ(add(-5, 5), 0);
}
然后运行:
ctest -V
你会看到每个 TEST 宏定义的用例都被自动识别并执行了。我个人非常喜欢 gtest_discover_tests() 这个功能,它省去了手动一个个 add_test 的麻烦。
注意: 如果你的测试程序需要命令行参数,比如 --gtest_filter,可以在 gtest_discover_tests() 之后用 set_tests_properties 添加。但更推荐的做法是直接在 ctest 命令行传参:ctest --test-args --gtest_filter=MathTest.*
23.5 测试的组织与分层
项目大了,测试不能乱放。我一般会这样组织目录结构:
project/
├── CMakeLists.txt
├── src/
│ └── math.cpp
├── test/
│ ├── CMakeLists.txt
│ ├── test_math.cpp
│ └── test_memory.cpp
在 test/CMakeLists.txt 中,我会为每个测试文件单独编译一个可执行文件:
add_executable(test_math test_math.cpp)
target_link_libraries(test_math GTest::GTest GTest::Main)
gtest_discover_tests(test_math)
add_executable(test_memory test_memory.cpp)
target_link_libraries(test_memory GTest::GTest GTest::Main)
gtest_discover_tests(test_memory)
这样做的好处是:每个测试程序独立编译,互不干扰。如果某个测试编译失败,不会影响其他测试。
23.6 核心逻辑流程图
下面这张图展示了 CMake 测试体系的整体流程,从配置到运行:
23.7 避坑指南
最后,分享几个我踩过的坑:
- enable_testing 位置不对:我曾经把它放在
add_subdirectory里面,结果 ctest 死活找不到测试。记住,它必须在顶层调用。 - GoogleTest 版本不匹配:CMake 3.10 以下对 GoogleTest 的支持不太好。我建议至少用 CMake 3.14,这样
FetchContent和gtest_discover_tests都能用得很顺。 - 测试程序没链接 GTest::Main:如果你不链接
GTest::Main,就需要自己写main()函数调用RUN_ALL_TESTS()。我个人觉得直接用GTest::Main更省事。 - ctest 输出乱码:在 Windows 上遇到过,原因是测试程序输出的是 UTF-8,但终端是 GBK。解决方案是在
set_tests_properties中设置ENVIRONMENT变量强制 UTF-8。
好了,CMake 测试这块的内容就这些。从 enable_testing 到 add_test,再到 GoogleTest 的集成,其实核心逻辑并不复杂。你只要把流程走通一次,后面就是复制粘贴的体力活了。
我的建议: 新项目从一开始就把测试框架搭好。哪怕只写一个测试用例,也比没有强。等代码量上来再补测试,那成本可就高了。
公众号:蓝海资料掘金营,微信deep3321