4. Google Test 深度剖析:断言机制、Test Fixture、参数化测试、死亡测试、事件监听器
Google Test(简称 GTest)是我在嵌入式项目中用得最多的 C++ 测试框架。说实话,它比很多商业工具都顺手。今天我把它的核心机制掰开揉碎了讲,每个点我都会结合项目里的实际坑来说。
4.1 断言机制:不只是 ASSERT_EQ
GTest 的断言分两类:ASSERT_* 和 EXPECT_*。区别很简单——ASSERT 失败直接终止当前测试,EXPECT 失败继续往下跑。
核心原则:用 EXPECT 检查非致命条件,用 ASSERT 检查致命条件。比如指针判空必须用 ASSERT,否则后面解引用直接崩。
我个人习惯这样分:
| 断言类型 | 适用场景 | 我踩过的坑 |
|---|---|---|
EXPECT_EQ / EXPECT_NE |
常规值比较,失败后继续检查其他条件 | 浮点数比较别用 EQ,精度问题坑了我一整天 |
ASSERT_TRUE / ASSERT_FALSE |
布尔条件,失败后没必要继续 | 资源初始化检查必须用 ASSERT |
EXPECT_FLOAT_EQ |
浮点数比较,内置 4 ULP 容差 | 默认容差够用,别手写 fabs |
ASSERT_DEATH |
验证程序是否按预期崩溃 | 后面死亡测试会细讲 |
小技巧:自定义类型重载 << 运算符,断言失败时 GTest 会自动打印可读信息。我在调试协议栈时靠这个省了大量时间。
4.2 Test Fixture:复用测试环境
嵌入式测试最烦的就是重复初始化硬件。Test Fixture 就是干这个的。
class UartFixture : public ::testing::Test {
protected:
UartDevice uart;
void SetUp() override {
uart = UartDevice(USART1);
uart.init(115200, 8, 'N', 1);
ASSERT_TRUE(uart.isReady());
}
void TearDown() override {
uart.deinit();
}
};
TEST_F(UartFixture, SendData) {
int ret = uart.send("Hello", 5);
EXPECT_EQ(ret, 5);
}
TEST_F(UartFixture, ReceiveTimeout) {
uint8_t buf[16];
int ret = uart.receive(buf, 16, 100);
EXPECT_EQ(ret, -ETIMEDOUT);
}
注意看,TEST_F 而不是 TEST。每个 TEST_F 都会独立调用 SetUp/TearDown,互不干扰。我曾经见过有人把 Fixture 当全局变量用,结果测试之间互相污染——嗯,那场面相当混乱。
避坑指南:SetUp 里别做耗时操作。我有个项目在 SetUp 里初始化 SD 卡,每次 200ms,300 个测试跑下来快一分钟。后来改成懒加载,速度提升 10 倍。
4.3 参数化测试:一套代码测所有
嵌入式里最典型的就是测不同波特率、不同数据长度。手写 20 个 TEST 太蠢了,参数化测试就是为这个生的。
class UartParamTest : public ::testing::TestWithParam<std::tuple<int, int, char>> {
protected:
UartDevice uart;
void SetUp() override {
auto [baud, dataBits, parity] = GetParam();
uart = UartDevice(USART2);
uart.init(baud, dataBits, parity, 1);
}
};
TEST_P(UartParamTest, LoopbackTest) {
uint8_t txData[] = {0xAA, 0xBB, 0xCC};
uart.send(txData, 3);
uint8_t rxData[3] = {0};
uart.receive(rxData, 3, 1000);
for(int i = 0; i < 3; i++) {
EXPECT_EQ(txData[i], rxData[i]);
}
}
INSTANTIATE_TEST_SUITE_P(
UartCombinations,
UartParamTest,
::testing::Combine(
::testing::Values(9600, 115200, 921600),
::testing::Values(7, 8),
::testing::Values('N', 'E', 'O')
)
);
这段代码会生成 3×2×3 = 18 个测试用例。每个用例名会自动带上参数值,失败时一眼就能定位。
实战经验:参数化测试特别适合做边界值测试。我曾在 Modbus 协议栈上用参数化测了 200 多种报文组合,发现 3 个隐藏极深的溢出 bug。
4.4 死亡测试:验证崩溃行为
嵌入式里有些函数就是设计成「不死不休」的——比如断言失败、看门狗触发、空指针解引用。死亡测试就是专门验证这些「预期内的崩溃」。
void dangerousFunction(void* ptr) {
if(ptr == nullptr) {
// 模拟硬件异常
while(1); // 死循环
}
*(volatile int*)ptr = 42;
}
TEST(DeathTest, NullPointerCrash) {
EXPECT_DEATH({
dangerousFunction(nullptr);
}, ".*");
}
TEST(DeathTest, AssertFailure) {
ASSERT_DEATH({
assert(1 == 2);
}, ".*assert.*failed.*");
}
为什么叫「死亡测试」?因为 GTest 会 fork 子进程执行代码,如果子进程挂了就算通过。但嵌入式环境往往没有 fork——这时候就要用 EXPECT_EXIT 配合自定义退出码。
我曾经踩过的坑:在 FreeRTOS 里用死亡测试,子进程 fork 后任务调度全乱。后来改成在模拟器环境跑死亡测试,真机只跑常规测试。
4.5 事件监听器:定制测试流程
事件监听器是 GTest 最被低估的功能。它让你在测试生命周期的各个节点插入自定义逻辑。
class CustomEventListener : public ::testing::EmptyTestEventListener {
public:
void OnTestStart(const ::testing::TestInfo& info) override {
printf("[START] %s.%s\n",
info.test_suite_name(), info.name());
// 记录开始时间
startTime = getCurrentMillis();
}
void OnTestEnd(const ::testing::TestInfo& info) override {
uint32_t elapsed = getCurrentMillis() - startTime;
if(info.result()->Passed()) {
printf("[PASS] %s.%s (%d ms)\n",
info.test_suite_name(), info.name(), elapsed);
} else {
printf("[FAIL] %s.%s\n",
info.test_suite_name(), info.name());
}
}
void OnTestSuiteStart(const ::testing::TestSuite& suite) override {
printf("===== Suite: %s =====\n", suite.name());
}
private:
uint32_t startTime;
};
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
::testing::TestEventListeners& listeners =
::testing::UnitTest::GetInstance()->listeners();
listeners.Append(new CustomEventListener);
return RUN_ALL_TESTS();
}
这个监听器做了三件事:打印测试开始/结束、统计执行时间、按套件分组输出。我在 CI 流水线上用这个生成测试报告,比默认输出清晰 10 倍。
进阶用法:监听器可以叠加。我习惯加一个 XML 报告生成器、一个失败截图器、一个性能统计器。三个监听器各司其职,互不干扰。
4.6 知识体系总览
下面这张图把 GTest 的核心机制串起来了。你写测试时心里要有这个结构。
4.7 实战组合拳
最后分享一个我在实际项目里的用法。一个典型的嵌入式模块测试,我会这样组合:
- Fixture 管理硬件初始化/反初始化
- 参数化 覆盖所有工作模式
- EXPECT_* 检查功能正确性
- ASSERT_* 检查前置条件
- 死亡测试 验证异常处理
- 监听器 输出结构化报告
这套组合拳我用了三年,覆盖了 20 多个嵌入式项目。说句实话,GTest 的深度远超大多数人的认知——它不只是个断言库,而是一整套测试基础设施。