2. 嵌入式软件测试基础:测试金字塔、单元测试、集成测试、系统测试在嵌入式领域的特殊要求

聊嵌入式软件测试,咱们得先搞清楚一个核心问题:测试到底该怎么分层?

很多人一上来就盯着系统测试,觉得那才是“真测试”。但我跟你说,这种想法在嵌入式领域特别危险。你想想看,一个跑在裸机上的驱动函数,如果单元级就没测透,等集成到 RTOS 里再查,定位问题的成本能翻几十倍。

我个人习惯,不管项目多紧,都会先画一张测试金字塔,然后对着它分配资源。这张图,说白了就是测试策略的骨架。

核心观点: 嵌入式测试金字塔不是摆设,它是资源投入的指挥棒。底层(单元测试)投入越多,后期返工越少。

2.1 测试金字塔在嵌入式场景下的变形

传统的测试金字塔长这样:底层是大量单元测试,中间是集成测试,顶层是少量系统测试。但在嵌入式世界里,这个金字塔得做点调整。

为什么?因为嵌入式系统有硬件依赖。你没法像纯软件那样,在开发机上随便 mock 一个 GPIO 寄存器就完事。硬件行为有时候是“非确定性的”——比如一个中断的触发时机,跟总线仲裁、DMA 传输都有关系。

我遇到过的一个坑:某次做车载 CAN 节点测试,团队在单元测试阶段 mock 了 CAN 控制器,所有用例全绿。结果集成到实车后,总线负载一高,消息就丢帧。查了三天,发现是 mock 层没模拟出“总线仲裁失败”这个状态。

所以,嵌入式测试金字塔,我建议你理解成下面这张图:

嵌入式软件测试金字塔(含硬件层级) 系统测试(实机/半实物) 集成测试(模块间 + 硬件接口) 协议栈集成 | 驱动-应用耦合 | 中断交互 单元测试(函数/模块级) 纯逻辑测试 | 硬件抽象层(HAL) Mock | 寄存器级仿真 硬件依赖层(特殊关注区) BSP 验证 | 外设驱动自测 | 时序约束检查 (此层不可 Mock,必须用真实硬件或精确仿真器) 💡 嵌入式测试金字塔的关键差异: • 硬件依赖层是嵌入式独有的“地基”,不可跳过 • 单元测试中,HAL Mock 的质量直接决定集成测试的可靠性 • 系统测试必须包含“硬件在环”(HIL) 场景,纯软件仿真不够
我的经验: 在资源受限的 MCU 项目里,我通常把测试资源按 50%(单元)、30%(集成)、20%(系统)分配。如果硬件还没出来,单元测试比例可以提到 70%,用模拟器先跑通逻辑。

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,那就测不到“发送缓冲区满”时的阻塞逻辑。

⚠️ 我曾经踩过的坑: 在 Mock 定时器中断时,只模拟了“中断触发”,没模拟“中断标志位自动清除”。结果单元测试全过,集成后中断一直重入,系统直接死机。记住:Mock 要模拟完整的状态机,不只是返回值。

2.3 集成测试:把积木搭起来,看会不会倒

单元测试过了,不代表模块之间能好好配合。集成测试,就是验证这些接口。

嵌入式集成测试有几个特殊关注点:

  • 中断优先级与嵌套: 两个模块各自的中断服务函数单独测都没问题,但合在一起,优先级低的可能永远得不到响应。
  • 共享资源竞争: 比如两个任务同时访问一个全局变量,单元测试里是串行的,集成后并发跑,数据就乱了。
  • 时序耦合: 模块 A 给模块 B 发消息,B 必须在 10ms 内处理完。单元测试里你 mock 了时间,集成后得用真实定时器量。

我建议的集成测试策略是“自底向上,逐层递增”:

集成阶段 测试内容 典型工具/方法
阶段一 驱动层 + HAL 集成 硬件循环测试、示波器抓波形
阶段二 中间件(协议栈/文件系统)集成 回环测试、压力测试
阶段三 应用层 + 中间件集成 基于主机的仿真(如 QEMU)
阶段四 全系统集成(含硬件) 硬件在环(HIL)测试
关键指标: 集成测试阶段,我特别关注“接口覆盖率”——每个模块间的函数调用、全局变量访问、消息传递路径是否都被测到。这个指标比代码行覆盖率更能反映集成风险。

2.4 系统测试:在真实世界里跑一遍

系统测试,就是把整个嵌入式系统放在目标环境里跑。这里说的“环境”,不只是硬件,还包括外部负载、电磁干扰、温度变化。

你想想看,一个在实验室里跑得好好的电机控制程序,一上产线,旁边有大功率变频器,结果控制信号被干扰,电机直接飞车。这种问题,单元测试和集成测试都很难发现。

嵌入式系统测试的特殊要求,我总结了几条:

  • 硬件在环(HIL): 用实时仿真器模拟外部物理环境,比如给 ECU 输入模拟的传感器信号,看它怎么响应。
  • 长时间稳定性测试: 嵌入式设备经常要 7×24 小时运行。我见过一个内存泄漏问题,跑了 72 小时才暴露出来。
  • 边界条件测试: 比如 ADC 采样值在 0 和 4095 这两个端点,或者看门狗超时前 1ms 喂狗。
  • 异常注入测试: 人为制造错误——比如拔掉传感器、短路通信线、拉低供电电压——看系统能不能安全恢复。
一个小技巧: 在做系统测试时,我习惯在代码里埋一些“测试桩”,比如通过串口打印内部状态变量。这样在实机测试时,不用接 JTAG 调试器也能看到关键数据。当然,正式发布前要记得把这些桩去掉。

2.5 嵌入式测试的“不可能三角”与取舍

做嵌入式测试久了,你会发现一个现实:测试覆盖率、测试速度、硬件可用性这三者很难同时满足。

举个例子:你想达到 100% 的 MCU 引脚电平覆盖率,那就得用昂贵的自动化测试设备,而且测试速度很慢。但如果你只做软件模拟,速度快了,覆盖率又上不去。

我个人习惯的做法是:

  1. 在硬件出来前: 用模拟器跑单元测试和部分集成测试,重点覆盖算法逻辑和协议解析。
  2. 硬件到手后: 先跑一遍“冒烟测试”——确认所有外设都能正常读写。然后逐步替换掉 Mock,跑真实的集成测试。
  3. 系统测试阶段: 用 HIL 系统跑自动化回归,每次代码提交都触发一次全量测试。
⚠️ 注意: 不要迷信 100% 覆盖率。我见过一个团队,为了把分支覆盖率从 95% 提到 100%,花了三周时间写测试用例,结果那 5% 的分支是死代码,永远不会执行。测试资源要用在刀刃上——优先覆盖关键路径和异常处理。