第十四章:测试集成——让代码质量不再靠“感觉”
说实话,我见过太多嵌入式项目,测试全靠开发人员拍胸脯说“我觉得没问题”。结果呢?硬件联调时 bug 一个接一个冒出来,定位问题比写代码还痛苦。我自己早期做 STM32 项目时也踩过这个坑,后来痛定思痛,把测试集成到 CMake 构建体系里,才算是真正解脱了。
这一章,我们就来聊聊怎么用 CMake 把测试这件事系统化。说白了,就是让机器替我们做重复劳动,而不是靠人肉测试。
14.1 测试框架的基石:enable_testing() 和 add_test()
CMake 内置了一套轻量级的测试机制。你不需要装任何第三方工具,就能跑起来最基本的单元测试。怎么用?两步走。
第一步:在顶层 CMakeLists.txt 中开启测试
cmake_minimum_required(VERSION 3.10)
project(MyEmbeddedProject C CXX)
# 开启测试支持
enable_testing()
add_subdirectory(src)
add_subdirectory(test)
注意,enable_testing() 必须在顶层 CMakeLists.txt 中调用。子目录里调了也没用,这是 CMake 的规则。
第二步:在 test 目录下添加测试用例
# test/CMakeLists.txt
add_executable(test_math test_math.c)
target_link_libraries(test_math PRIVATE mylib)
# 注册测试
add_test(NAME test_math COMMAND test_math)
这里 add_test 的第一个参数是测试名称,第二个参数是可执行文件。你可以加多个测试,比如:
add_test(NAME test_math COMMAND test_math)
add_test(NAME test_gpio COMMAND test_gpio)
add_test(NAME test_uart COMMAND test_uart)
然后怎么跑?用 ctest 命令:
mkdir build && cd build
cmake ..
cmake --build .
ctest
输出会告诉你每个测试是 PASS 还是 FAIL。简单粗暴,但够用。
WORKING_DIRECTORY 属性,确保测试在正确的目录下运行。比如:
set_tests_properties(test_math PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
这样就不会因为路径问题导致测试失败。
14.2 CTest:不只是跑测试那么简单
CTest 是 CMake 自带的测试驱动工具。它不仅能跑测试,还能做很多事情。我挑几个常用的功能说说。
并行测试
嵌入式项目测试多了,一个个跑很慢。CTest 支持并行:
ctest -j4 # 同时跑4个测试
测试超时控制
嵌入式测试有时会卡死,比如某个外设没响应。设置超时很有必要:
set_tests_properties(test_uart PROPERTIES TIMEOUT 10)
超过10秒,CTest 会自动杀掉测试进程并标记为 FAIL。
测试标签分类
你可以给测试打标签,然后按标签筛选运行:
set_tests_properties(test_math PROPERTIES LABELS "unit")
set_tests_properties(test_gpio PROPERTIES LABELS "integration")
# 只跑单元测试
ctest -L unit
这个功能我特别喜欢。在 CI 流水线里,我可以让单元测试每次提交都跑,集成测试只在合并前跑一次,节省大量时间。
| CTest 命令 | 作用 | 我常用的场景 |
|---|---|---|
| ctest -jN | 并行运行 N 个测试 | 本地开发时加速 |
| ctest -L tag | 按标签筛选测试 | CI 中区分单元/集成测试 |
| ctest -R regex | 按名称正则匹配 | 只跑某个模块的测试 |
| ctest --output-on-failure | 失败时打印详细输出 | 调试测试失败原因 |
ctest -E 排除那些需要硬件的测试。否则测试会一直挂在那,直到超时。
14.3 Google Test 集成:让测试更专业
CMake 自带的测试机制虽然简单,但功能有限。比如没有断言宏、没有测试夹具、没有参数化测试。这时候,Google Test 就派上用场了。
集成步骤
第一步:下载 Google Test 源码。我习惯用 FetchContent 自动拉取:
# 顶层 CMakeLists.txt
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/release-1.12.1.zip
)
FetchContent_MakeAvailable(googletest)
第二步:编写测试用例
// test/test_math.cpp
#include <gtest/gtest.h>
#include "math_utils.h"
TEST(MathTest, Add) {
EXPECT_EQ(add(2, 3), 5);
EXPECT_EQ(add(-1, 1), 0);
}
TEST(MathTest, Divide) {
EXPECT_EQ(divide(10, 2), 5);
EXPECT_THROW(divide(1, 0), std::invalid_argument);
}
第三步:在 test/CMakeLists.txt 中链接
add_executable(test_math test_math.cpp)
target_link_libraries(test_math PRIVATE mylib gtest gtest_main)
add_test(NAME test_math COMMAND test_math)
注意,我链接了 gtest_main,它会自动生成 main 函数。如果你有自定义的 main,就只链接 gtest。
测试夹具(Test Fixture)
嵌入式开发中,经常需要初始化硬件、分配内存。用夹具可以避免重复代码:
class GpioTest : public ::testing::Test {
protected:
void SetUp() override {
// 模拟 GPIO 初始化
gpio_init();
}
void TearDown() override {
// 清理
gpio_deinit();
}
};
TEST_F(GpioTest, SetHigh) {
gpio_set(GPIO_PIN_0, HIGH);
EXPECT_EQ(gpio_read(GPIO_PIN_0), HIGH);
}
你想想看,如果没有夹具,每个测试都要写一遍初始化代码,多烦人。
14.4 嵌入式测试的特殊考量
嵌入式测试和纯软件测试不太一样。我总结了几点经验:
- 模拟硬件层: 用 C++ 的多态或 C 的函数指针,把硬件操作替换成模拟实现。比如 GPIO 读写可以替换成内存操作。
- 交叉编译测试: 如果测试必须在目标板上跑,可以用
add_test的COMMAND指定远程执行脚本,比如通过 SSH 上传并运行。 - 内存泄漏检测: 嵌入式系统内存有限,我习惯在测试中集成 AddressSanitizer 或 Valgrind。
14.5 知识体系总览
下面这张图,是我对本章知识点的梳理。你可以把它当作一个快速参考。
嗯,这张图基本把本章的核心内容串起来了。你从底层内置机制开始,往上到 CTest 工具,再到 Google Test 的高级特性,最后落到嵌入式特有的考量。每一层都有它的用途。
add_test,跑通再说。等团队习惯了测试流程,再逐步引入 Google Test 和 CI 集成。步子迈太大,容易扯着蛋。
好了,测试集成这部分就聊到这。记住一句话:测试不是负担,是投资。你花在测试上的每一分钟,都会在后期调试时加倍还给你。