2. 嵌入式软件测试基础:测试金字塔、单元测试、集成测试、系统测试在嵌入式领域的特殊要求
聊嵌入式软件测试,咱们得先搞清楚一个核心问题:测试到底该怎么分层?
很多人一上来就盯着系统测试,觉得那才是“真测试”。但我跟你说,这种想法在嵌入式领域特别危险。你想想看,一个跑在裸机上的驱动函数,如果单元级就没测透,等集成到 RTOS 里再查,定位问题的成本能翻几十倍。
我个人习惯,不管项目多紧,都会先画一张测试金字塔,然后对着它分配资源。这张图,说白了就是测试策略的骨架。
2.1 测试金字塔在嵌入式场景下的变形
传统的测试金字塔长这样:底层是大量单元测试,中间是集成测试,顶层是少量系统测试。但在嵌入式世界里,这个金字塔得做点调整。
为什么?因为嵌入式系统有硬件依赖。你没法像纯软件那样,在开发机上随便 mock 一个 GPIO 寄存器就完事。硬件行为有时候是“非确定性的”——比如一个中断的触发时机,跟总线仲裁、DMA 传输都有关系。
我遇到过的一个坑:某次做车载 CAN 节点测试,团队在单元测试阶段 mock 了 CAN 控制器,所有用例全绿。结果集成到实车后,总线负载一高,消息就丢帧。查了三天,发现是 mock 层没模拟出“总线仲裁失败”这个状态。
所以,嵌入式测试金字塔,我建议你理解成下面这张图:
2.2 单元测试:嵌入式里的“显微镜”
单元测试,测的是最小可测单元——通常是一个函数或者一个模块。在嵌入式里,这事没那么简单。
你写了一个 uart_send_byte(),它直接操作 UART->DR 寄存器。你怎么测?总不能每次都往真串口上发数据吧?
这里就要引入 硬件抽象层(HAL)Mock。说白了,就是把底层硬件操作替换成可控的模拟函数。
// 原函数:直接操作寄存器
void uart_send_byte(UART_TypeDef *uart, uint8_t data) {
while (!(uart->SR & UART_SR_TXE)); // 等待发送缓冲区空
uart->DR = data;
}
// 测试时:通过 Mock 替换寄存器访问
// mock_uart.c
static uint8_t mock_dr_value;
static uint32_t mock_sr_value = UART_SR_TXE;
void mock_uart_set_sr(uint32_t sr) { mock_sr_value = sr; }
uint8_t mock_uart_get_dr(void) { return mock_dr_value; }
// 测试用例
void test_uart_send_byte_should_write_data_to_dr() {
mock_uart_set_sr(UART_SR_TXE); // 模拟发送就绪
uart_send_byte(&mock_uart, 0xAB);
assert(mock_uart_get_dr() == 0xAB);
}
你看,通过 Mock,我们把一个依赖硬件的函数变成了纯逻辑测试。这里有个关键点:Mock 的粒度要跟硬件行为匹配。比如上面那个 UART_SR_TXE 标志位,如果你 Mock 成一直为 1,那就测不到“发送缓冲区满”时的阻塞逻辑。
2.3 集成测试:把积木搭起来,看会不会倒
单元测试过了,不代表模块之间能好好配合。集成测试,就是验证这些接口。
嵌入式集成测试有几个特殊关注点:
- 中断优先级与嵌套: 两个模块各自的中断服务函数单独测都没问题,但合在一起,优先级低的可能永远得不到响应。
- 共享资源竞争: 比如两个任务同时访问一个全局变量,单元测试里是串行的,集成后并发跑,数据就乱了。
- 时序耦合: 模块 A 给模块 B 发消息,B 必须在 10ms 内处理完。单元测试里你 mock 了时间,集成后得用真实定时器量。
我建议的集成测试策略是“自底向上,逐层递增”:
| 集成阶段 | 测试内容 | 典型工具/方法 |
|---|---|---|
| 阶段一 | 驱动层 + HAL 集成 | 硬件循环测试、示波器抓波形 |
| 阶段二 | 中间件(协议栈/文件系统)集成 | 回环测试、压力测试 |
| 阶段三 | 应用层 + 中间件集成 | 基于主机的仿真(如 QEMU) |
| 阶段四 | 全系统集成(含硬件) | 硬件在环(HIL)测试 |
2.4 系统测试:在真实世界里跑一遍
系统测试,就是把整个嵌入式系统放在目标环境里跑。这里说的“环境”,不只是硬件,还包括外部负载、电磁干扰、温度变化。
你想想看,一个在实验室里跑得好好的电机控制程序,一上产线,旁边有大功率变频器,结果控制信号被干扰,电机直接飞车。这种问题,单元测试和集成测试都很难发现。
嵌入式系统测试的特殊要求,我总结了几条:
- 硬件在环(HIL): 用实时仿真器模拟外部物理环境,比如给 ECU 输入模拟的传感器信号,看它怎么响应。
- 长时间稳定性测试: 嵌入式设备经常要 7×24 小时运行。我见过一个内存泄漏问题,跑了 72 小时才暴露出来。
- 边界条件测试: 比如 ADC 采样值在 0 和 4095 这两个端点,或者看门狗超时前 1ms 喂狗。
- 异常注入测试: 人为制造错误——比如拔掉传感器、短路通信线、拉低供电电压——看系统能不能安全恢复。
2.5 嵌入式测试的“不可能三角”与取舍
做嵌入式测试久了,你会发现一个现实:测试覆盖率、测试速度、硬件可用性这三者很难同时满足。
举个例子:你想达到 100% 的 MCU 引脚电平覆盖率,那就得用昂贵的自动化测试设备,而且测试速度很慢。但如果你只做软件模拟,速度快了,覆盖率又上不去。
我个人习惯的做法是:
- 在硬件出来前: 用模拟器跑单元测试和部分集成测试,重点覆盖算法逻辑和协议解析。
- 硬件到手后: 先跑一遍“冒烟测试”——确认所有外设都能正常读写。然后逐步替换掉 Mock,跑真实的集成测试。
- 系统测试阶段: 用 HIL 系统跑自动化回归,每次代码提交都触发一次全量测试。