欢迎光临
我们一直在努力

【C++ 硬核】摆脱开发板:用 Google Test + Mock 构建嵌入式 TDD (测试驱动开发) 体系

摘要:嵌入式软件质量往往依赖于手工测试,回归测试成本极高。一旦底层硬件没就位,软件开发就得停滞。本文将介绍如何通过 接口抽象 和 依赖注入,将业务逻辑与硬件驱动解耦。利用 Google Mock 模拟硬件行为(如模拟 Flash 写入失败、模拟传感器数据),在 PC 上实现自动化的单元测试。


一、 痛点:被硬件“绑架”的软件开发

假设你要写一个 “数据记录器” 的逻辑:

  • 每隔 1 秒读取传感器。

  • 如果数据超过阈值,写入 Flash。

  • 如果 Flash 写满了,擦除最旧的一个扇区。

  • 典型的“耦合”代码

    // DataLogger.cpp
    #include "stm32f4xx_hal.h" // 强依赖硬件库

    void LogProcess() {
    float val = AD7606_Read(); // 直接调用驱动

    if (val > 50.0f) {
    if (W25Q_Write(val) != HAL_OK) { // 直接调用驱动
    W25Q_EraseSector(0);
    }
    }
    }

    问题:

  • 无法测试:想测试“Flash 写满擦除”的逻辑,你必须真的把 Flash 写满(可能需要几个小时),或者去改驱动代码造假数据。

  • 无法移植:这个代码里全是 STM32 的头文件,换个芯片要重写。

  • 开发阻塞:板子还没画回来,你的代码就没法跑。


  • 二、 破局:面向接口编程 (Interface-Based Design)

    要实现 PC 端测试,必须把**“做什么 (Logic)”** 和 “怎么做 (Driver)” 分开。

    1. 定义纯虚接口 (HAL Abstraction)

    // IFlash.h
    // 定义 Flash 的抽象行为,不包含任何 STM32 代码
    class IFlash {
    public:
    virtual ~IFlash() = default;
    virtual bool Write(uint32_t addr, const uint8_t* data, size_t len) = 0;
    virtual bool Erase(uint32_t addr) = 0;
    };

    // ISensor.h
    class ISensor {
    public:
    virtual ~ISensor() = default;
    virtual float ReadVoltage() = 0;
    };

    2. 编写业务逻辑 (只依赖接口)

    // DataLogger.h
    #include "IFlash.h"
    #include "ISensor.h"

    class DataLogger {
    IFlash& m_flash; // 引用接口,而非具体类
    ISensor& m_sensor;

    public:
    // 依赖注入:在构造时传入具体的实现
    DataLogger(IFlash& flash, ISensor& sensor)
    : m_flash(flash), m_sensor(sensor) {}

    void Process() {
    float val = m_sensor.ReadVoltage();
    if (val > 50.0f) {
    // 写入地址 0,模拟 4 字节数据
    bool success = m_flash.Write(0, (uint8_t*)&val, 4);
    if (!success) {
    // 如果写入失败,尝试擦除
    m_flash.Erase(0);
    }
    }
    }
    };

    三、 核心武器:Google Mock

    现在我们想测试 DataLogger。在 STM32 上,我们会传入真实的 Stm32Flash 类;但在 PC 上,我们传入一个**“骗子” (Mock Object)**。

    Google Mock 可以自动生成这个“骗子”,并允许我们控制它的行为。

    1. 定义 Mock 类

    #include <gmock/gmock.h>
    #include "IFlash.h"
    #include "ISensor.h"

    class MockFlash : public IFlash {
    public:
    // MOCK_METHOD(返回值, 函数名, (参数…), (修饰符));
    MOCK_METHOD(bool, Write, (uint32_t, const uint8_t*, size_t), (override));
    MOCK_METHOD(bool, Erase, (uint32_t), (override));
    };

    class MockSensor : public ISensor {
    public:
    MOCK_METHOD(float, ReadVoltage, (), (override));
    };

    四、 实战:编写单元测试用例

    我们不需要编译到 ARM,直接用 gcc/clang 编译成 PC 的 exe 运行。

    测试场景 1:正常记录数据

    #include <gtest/gtest.h>

    TEST(LoggerTest, ShouldWriteWhenVoltageHigh) {
    // 1. 准备 Mock 对象
    MockFlash flash;
    MockSensor sensor;
    DataLogger logger(flash, sensor);

    // 2. 设置期望 (Expectations)
    // 当调用 sensor.ReadVoltage 时,请返回 60.0 (超过阈值)
    EXPECT_CALL(sensor, ReadVoltage()).WillOnce(testing::Return(60.0f));

    // 期待 flash.Write 被调用一次,且返回 true
    EXPECT_CALL(flash, Write(0, testing::_, 4)).WillOnce(testing::Return(true));

    // 3. 执行业务
    logger.Process();
    }

    测试场景 2:Flash 写满自动擦除 (很难在板子上测!)

    TEST(LoggerTest, ShouldEraseWhenWriteFails) {
    MockFlash flash;
    MockSensor sensor;
    DataLogger logger(flash, sensor);

    // 1. 模拟电压超限
    EXPECT_CALL(sensor, ReadVoltage()).WillOnce(testing::Return(60.0f));

    // 2. 【关键】模拟 Flash 写入失败 (返回 false)
    EXPECT_CALL(flash, Write(testing::_, testing::_, testing::_))
    .WillOnce(testing::Return(false));

    // 3. 验证:由于写入失败,logger 应该调用 Erase
    EXPECT_CALL(flash, Erase(0)).Times(1);

    // 4. 执行
    logger.Process();
    }

    五、 进阶技巧:如何 Mock 只有 C 接口的库?

    很多时候我们无法修改底层代码,比如 HAL 库只有 C 函数 HAL_GPIO_WritePin。 这时候可以使用 Linker Seam (链接器接缝) 或 虚函数适配器。

    推荐做法:适配器模式 (Adapter Pattern)

    不要直接在业务代码里调 HAL_xxx。

  • 定义 IGpio C++ 接口。

  • 写一个 Stm32Gpio 类实现该接口,内部调用 HAL_GPIO_WritePin。

  • 业务代码只用 IGpio。

  • 测试代码 Mock IGpio。

  • 硬核做法:弱符号覆盖 (Weak Symbol Override)

    如果 HAL 库里的函数是 __weak 的(STM32 HAL 大部分中断回调都是 weak),你可以在测试工程里重新定义这个 C 函数,在里面植入测试逻辑。


    六、 为什么这是“降维打击”?

  • 速度:PC 运行测试只需要 0.1 秒。板子烧录运行需要 2 分钟。

  • 覆盖率:你能测试“Flash 损坏”、“I2C 总线超时”、“网络断连”等硬件上极难复现的异常情况。

  • 重构底气:当你优化算法时,跑一遍测试,全绿。你敢保证代码没改坏。如果没有测试,你改一行代码都心惊胆战。

  • 架构优化:为了能测试,你被迫把代码写成“低耦合”的接口形式。代码结构变好了,是测试带来的副作用。


  • 七、 总结

    嵌入式开发不应该等同于“硬件调试”。

    通过引入 Google Test 和 Mock 技术,我们将嵌入式开发拆解为两部分:

  • 在 Host 端:通过 TDD 验证 95% 的业务逻辑、状态机跳转、协议解析。

  • 在 Target 端:只验证剩下的 5% —— 驱动是不是真的能点亮 LED。

  • 这就是现代嵌入式软件工程:用软件的思维写嵌入式,而不是用电工的思维写代码。

    赞(0)
    未经允许不得转载:171主机测评 » 【C++ 硬核】摆脱开发板:用 Google Test + Mock 构建嵌入式 TDD (测试驱动开发) 体系
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址