【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; };

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

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

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

Read more

Flutter 三方库 commander_ui 的鸿蒙化适配指南 - 构建大屏控制台风格 UI、支持指令式交互与极客风格面板

Flutter 三方库 commander_ui 的鸿蒙化适配指南 - 构建大屏控制台风格 UI、支持指令式交互与极客风格面板

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 commander_ui 的鸿蒙化适配指南 - 构建大屏控制台风格 UI、支持指令式交互与极客风格面板 前言 在进行 Flutter for OpenHarmony 开发时,某些特定场景(如物联网中控屏、服务器管理工具或黑客风格的极客应用)需要一种区别于常规 Material/Cupertino 的视觉风格。commander_ui 提供了一套模拟命令行交互与工业控制台风格的 UI 组件库。本文将探讨如何在鸿蒙端利用该库打造极具视觉冲击力的指挥中心界面。 一、原理解析 / 概念介绍 1.1 基础原理 commander_ui 基于 Flutter 的 CustomPaint 和灵活的层叠布局构建。它通过模拟扫描线、等宽字体以及高对比度的颜色方案,还原了经典终端与指挥大屏的视觉质感。 graph

By Ne0inhk
Flutter 三方库 super_log 的鸿蒙化适配指南 - 实现极具视觉冲击力的彩色终端日志、支持动态过滤与全局异常捕获

Flutter 三方库 super_log 的鸿蒙化适配指南 - 实现极具视觉冲击力的彩色终端日志、支持动态过滤与全局异常捕获

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 super_log 的鸿蒙化适配指南 - 实现极具视觉冲击力的彩色终端日志、支持动态过滤与全局异常捕获 前言 在进行 Flutter for OpenHarmony 的日常开发调试时,面对控制台里密密麻麻、死板单调的白色日志,开发者很容易在大海捞针般的排错过程中产生疲劳。super_log 是一个专注于日志可视化体验的增强库。它通过丰富的配色方案和清晰的结构化打印,让鸿蒙控制台里的每条日志都具备“辨识度”。本文将介绍如何在鸿蒙端利用 super_log 让你的代码“自白”得更加生动。 一、原理解析 / 概念介绍 1.1 基础原理 super_log 基于终端的 ANSI 颜色转义序列。它通过解析日志级别,并在输出字符串中自动嵌入特定的颜色代码。同时,它还内置了美观的边框修饰符(Box

By Ne0inhk
Flutter 组件 ansi_text 适配鸿蒙 HarmonyOS 实战:终端色彩渲染,构建高性能 ANSI 日志高亮与命令行交互架构

Flutter 组件 ansi_text 适配鸿蒙 HarmonyOS 实战:终端色彩渲染,构建高性能 ANSI 日志高亮与命令行交互架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 ansi_text 适配鸿蒙 HarmonyOS 实战:终端色彩渲染,构建高性能 ANSI 日志高亮与命令行交互架构 前言 在鸿蒙(OpenHarmony)生态迈向工业级运维、涉及大量后台守护进程(Daemon)、系统日志审计及开发者工具链(CLI)开发的背景下,如何为枯燥的纯文本终端注入具备视觉层级的色彩与样式,已成为提升调试效率与故障定位速度的“视觉助推器”。在鸿蒙设备这类强调 AOT 极致性能与低级别 shell 交互的环境下,如果应用依然依赖基础的单色字符串输出日志,由于由于信息流极其庞大且缺乏重点,极易由于由于“视觉疲劳”导致关键系统警告或业务异常被淹没在海量数据中。 我们需要一种能够支持 ANSI 转义序列、具备富文本样式(加粗/背景色)且兼容多种终端模拟器的文本渲染方案。 ansi_text 为 Flutter 开发者引入了基于标准

By Ne0inhk

Flutter 三方库 holiday_jp 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、全维度的日本法定节假日(公休日)查询与日历调度引擎

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 holiday_jp 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、全维度的日本法定节假日(公休日)查询与日历调度引擎 在鸿蒙(OpenHarmony)系统的全球化(Globalization)出海应用、针对日本市场的日程管理、财务结算系统(需考虑日本银行休假)或带有国际化特色的鸿蒙版日历组件中,如何瞬间获取任意年份日本的法定节假日、判定当前是否为公休日?holiday_jp 为开发者提供了一套工业级的、基于官方精细化数据集的日本节假日处理方案。本文将深入实战其在鸿蒙出海应用逻辑层中的应用。 前言 什么是 Holiday JP?它是一个专注于提供日本法定假期(祝日)数据的专业库。它涵盖了从传统的“元日”到现代的“体育之日”等所有官方假期,并能自动处理由于由于由于由于“振替休日(补休)”产生的动态调休逻辑。在 Flutter

By Ne0inhk