第一章我们跑通了环境,但这只是"纯软件"的逻辑。在嵌入式开发中,最让人头疼的是代码里随处可见的 HAL_GPIO_WritePin、__HAL_TIM_GET_COUNTER 等硬件依赖。
如果直接把这些带进测试,编译器会因为找不到 STM32 的寄存器定义而报错。这一章,我们要学习 TDD 的核心技术------隔离(Isolation)与伪装(Mocking)。
2.1 依赖倒置:不要直接调用 HAL 库
很多人的代码是这么写的:
// 坏习惯:业务逻辑和 HAL 库强耦合
void Toggle_Led_If_Expired(void) {
if (HAL_GetTick() > 1000) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
}
这段代码在 PC 上无法通过编译 ,因为 PC 没有 GPIOA。
TDD 的思维: 我们不直接调用底层,而是定义一套"硬件接口层"。 在 src/ 下创建 Hardware_Interface.h:
#ifndef HARDWARE_INTERFACE_H
#define HARDWARE_INTERFACE_H
#include <stdint.h>
#include <stdbool.h>
// 抽象后的接口,PC 和 STM32 都能识别
void HW_GPIO_SetLed(bool on);
uint32_t HW_GetSystemTick(void);
#endif
2.2 CMock 的魔法:mock_ 前缀
这是 Ceedling 最强大的功能。只要你在测试文件中 #include "mock_Hardware_Interface.h",Ceedling 就会:
-
扫描
Hardware_Interface.h。 -
自动生成 一个
mock_Hardware_Interface.c文件。 -
伪造 出
HW_GPIO_SetLed_Expect这种函数。
2.3 实战:编写一个延时点灯逻辑
我们要实现一个功能:调用 App_LightControl() 时,如果系统时间大于 500ms,就熄灭 LED。
第一步:编写测试 (test_App_Logic.c) 在 test/ 目录下创建文件。注意,我们先写测试,此时 App_LightControl 甚至还没被定义。
#include "unity.h"
#include "mock_Hardware_Interface.h" // 自动生成的 Mock 接口
#include "App_Logic.h"
void test_ShouldTurnOffLed_WhenTimeExceeds500ms(void) {
// 模拟上帝视角:
// 1. 我们预期逻辑会询问当前时间,我们让它返回 501
HW_GetSystemTick_ExpectAndReturn(501);
// 2. 因为 501 > 500,我们预期逻辑会调用 LED 设置函数,参数为 false (关灯)
HW_GPIO_SetLed_Expect(false);
// 3. 执行被测动作
App_LightControl();
}
void test_ShouldKeepLedOn_WhenTimeIsBelow500ms(void) {
// 1. 模拟时间还没到 500ms,返回 100
HW_GetSystemTick_ExpectAndReturn(100);
// 2. 这里不写 HW_GPIO_SetLed_Expect
// 如果代码在这个时候调用了该函数,测试会立即报错(Unexpected Call)
App_LightControl();
}
2.4 第二步:实现逻辑使测试通过
在 src/ 中创建 App_Logic.c:
#include "App_Logic.h"
#include "Hardware_Interface.h"
void App_LightControl(void) {
if (HW_GetSystemTick() > 500) {
HW_GPIO_SetLed(false);
}
}
2.5 深度解析:为什么这很重要?
-
确定性测试:在真实 STM32 上,你需要等 500ms 才能看到结果。在 Mock 环境下,时间是由你注入的。
-
硬件无关性:这段代码不需要连接任何开发板,在地铁上用笔记本就能完成开发。
-
接口契约 :这强迫你思考"我的逻辑到底需要哪些硬件资源",并把它们提炼成接口,从而实现了高内聚、低耦合。
本章小结
我们学会了如何使用 Expect(预期调用)和 ExpectAndReturn(预期调用并注入返回值)。这不仅能 Mock GPIO,还能 Mock 传感器读取、Flash 写入等一切硬件行为。