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

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


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

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

  1. 每隔 1 秒读取传感器。

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

  3. 如果 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);
        }
    }
}

问题

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

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

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


二、 破局:面向接口编程 (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;
};
  1. 编写业务逻辑 (只依赖接口)

    // 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

  1. 定义 IGpio C++ 接口。

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

  3. 业务代码只用 IGpio

  4. 测试代码 Mock IGpio

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

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


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

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

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

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

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


七、 总结

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

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

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

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

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

相关推荐
小龙报3 小时前
【51单片机】串口通讯从入门到精通:原理拆解 + 参数详解 + 51 单片机实战指南
c语言·驱动开发·stm32·单片机·嵌入式硬件·物联网·51单片机
dlz083619 小时前
POE驱动开发流程
驱动开发
嵌入式-老费21 小时前
Linux camera驱动开发(DVP接口的camera sensor)
驱动开发
VernonJsn2 天前
visual studio 2022的windows驱动开发
ide·驱动开发·visual studio
嵌入式郑工2 天前
RK3566 LubanCat 开发板 USB Gadget 配置完整复盘
linux·驱动开发·ubuntu
雾削木3 天前
树莓派 ESPHome 固件编译与烧录全攻略(解决超时与串口识别问题)
驱动开发
春日见4 天前
win11 分屏设置
java·开发语言·驱动开发·docker·单例模式·计算机外设
DarkAthena4 天前
【GaussDB】手动编译不同python版本的psycopg2驱动以适配airflow
驱动开发·python·gaussdb
松涛和鸣5 天前
DAY66 SPI Driver for ADXL345 Accelerometer
linux·网络·arm开发·数据库·驱动开发