跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

摆脱开发板限制:基于 Google Test + Mock 的嵌入式 TDD 实践

介绍如何通过接口抽象和依赖注入解耦嵌入式业务逻辑与硬件驱动,利用 Google Mock 在 PC 端模拟硬件行为进行自动化单元测试。通过定义纯虚接口、编写 Mock 类及链接器接缝技巧,实现在无硬件环境下验证 Flash 写入失败等异常场景,提升代码重构信心与测试覆盖率,实现现代嵌入式软件工程的高效开发。

无尘发布于 2026/3/26更新于 2026/10/24108 浏览

摘要:嵌入式软件质量往往依赖于手工测试,回归测试成本极高。一旦底层硬件没就位,软件开发就得停滞。本文将介绍如何通过 接口抽象 和 依赖注入,将业务逻辑与硬件驱动解耦。利用 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
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。

  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 Test 和 Mock 技术,我们将嵌入式开发拆解为两部分:

  1. 在 Host 端:通过 TDD 验证 95% 的业务逻辑、状态机跳转、协议解析。
  2. 在 Target 端:只验证剩下的 5% —— 驱动是不是真的能点亮 LED。

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

目录

  1. 一、痛点:被硬件“绑架”的软件开发
  2. 典型的“耦合”代码
  3. 二、破局:面向接口编程 (Interface-Based Design)
  4. 1. 定义纯虚接口 (HAL Abstraction)
  5. 2. 编写业务逻辑 (只依赖接口)
  6. 三、核心武器:Google Mock
  7. 1. 定义 Mock 类
  8. 四、实战:编写单元测试用例
  9. 测试场景 1:正常记录数据
  10. 测试场景 2:Flash 写满自动擦除 (很难在板子上测!)
  11. 五、进阶技巧:如何 Mock 只有 C 接口的库?
  12. 六、为什么这是“降维打击”?
  13. 七、总结

更多推荐文章

查看全部
  • Android 自定义 ViewGroup 九宫格布局完整实现
  • Spring AI 实现 RAG 检索增强生成详解与实践
  • 国内直连AI绘画工具深度实践指南:云端与本地部署对比
  • C++ 从入门到精通:完整实战指南
  • HarmonyOS6 RcButton 组件交互逻辑与事件处理机制
  • C++多态实现深度剖析:从抽象类约束到虚函数表机制
  • Go 语言企业级权限管理系统设计与实现
  • 基于 Flask 的酒店管理系统开发指南(PyCharm 环境)
  • 基于 YOLOv8 的无人机红外可见光光伏缺陷检测数据集与训练指南
  • Qwen2.5-VL 与 Janus-Pro-7B 视觉理解能力对比评测
  • 面试题解析:消失的两个数字(位运算解法)
  • 算法实战:位运算解决两数之和、唯一数字与缺失数字
  • 本地 CPU 环境部署 ChatGLM-6B 大模型与量化技术实践
  • Linux/C++进阶:man 手册、GDB 调试与静态动态库详解
  • OpenAkita:自我进化的开源 AI 助手框架
  • CentOS 7/8/9 安装 MySQL 8.0+ 完整指南
  • 麦橘超然 WebUI 新手指南:Flux 离线图像生成部署与实战
  • 量化交易中 Python 与 C++ 的选择考量
  • C++ 入门:历史、首个程序与命名空间详解
  • Rust 异步微服务架构最佳实践与反模式规避

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online