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 的核心机制串起来了。你写测试时心里要有这个结构。

Google Test 核心机制体系 断言机制 ASSERT_* 致命 EXPECT_* 非致命 浮点/字符串/自定义 Test Fixture SetUp / TearDown TEST_F 宏 环境隔离复用 参数化测试 TestWithParam<T> INSTANTIATE 组合/范围/值列表 死亡测试 EXPECT_DEATH ASSERT_DEATH 子进程隔离 事件监听器 OnTestStart / OnTestEnd OnTestSuiteStart / OnTestSuiteEnd 自定义报告 / 性能统计 / CI 集成 五大机制协同工作:断言提供检查能力,Fixture 管理环境 参数化扩展覆盖,死亡测试验证异常,监听器串联流程

4.7 实战组合拳

最后分享一个我在实际项目里的用法。一个典型的嵌入式模块测试,我会这样组合:

  1. Fixture 管理硬件初始化/反初始化
  2. 参数化 覆盖所有工作模式
  3. EXPECT_* 检查功能正确性
  4. ASSERT_* 检查前置条件
  5. 死亡测试 验证异常处理
  6. 监听器 输出结构化报告

这套组合拳我用了三年,覆盖了 20 多个嵌入式项目。说句实话,GTest 的深度远超大多数人的认知——它不只是个断言库,而是一整套测试基础设施。