23、CMake测试:enable_testing、add_test、ctest、GoogleTest集成

测试这件事,说实话,很多嵌入式工程师一开始都不太重视。我早年做项目时也是这样——总觉得「代码能跑就行」,测试嘛,手动点两下就完了。直到有一次,一个底层驱动的小改动,把整个协议栈搞崩了,排查了整整两天。嗯,从那以后,我再也不敢轻视自动化测试了。

CMake 对测试的支持其实很成熟。你不需要额外装什么复杂的框架,CMake 自带的 enable_testingadd_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 模块。

集成步骤其实就三步:

  1. 下载或引入 GoogleTest:推荐用 FetchContentadd_subdirectory
  2. 编写测试代码:用 TEST() 宏定义测试用例
  3. 链接并注册:用 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 测试体系的整体流程,从配置到运行:

CMake 测试体系核心流程 enable_testing() add_test() / gtest_discover_tests() cmake --build . ctest 测试通过 ✓ 测试失败 ✗ 顶层 CMakeLists.txt 注册测试用例 编译测试程序 运行所有测试

23.7 避坑指南

最后,分享几个我踩过的坑:

  • enable_testing 位置不对:我曾经把它放在 add_subdirectory 里面,结果 ctest 死活找不到测试。记住,它必须在顶层调用。
  • GoogleTest 版本不匹配:CMake 3.10 以下对 GoogleTest 的支持不太好。我建议至少用 CMake 3.14,这样 FetchContentgtest_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_testingadd_test,再到 GoogleTest 的集成,其实核心逻辑并不复杂。你只要把流程走通一次,后面就是复制粘贴的体力活了。

我的建议: 新项目从一开始就把测试框架搭好。哪怕只写一个测试用例,也比没有强。等代码量上来再补测试,那成本可就高了。


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