为什么需要桩函数与驱动?

嵌入式软件单元测试,说起来简单,做起来全是坑。我刚开始带项目的时候,团队写了个底层驱动模块,逻辑上觉得没问题,一跑测试——好家伙,依赖的硬件根本没准备好。那时候我就意识到,没有桩函数和驱动,单元测试在嵌入式领域就是个空话。

嵌入式单元测试的三大痛点

你想想看,嵌入式软件和普通桌面软件最大的区别是什么?

  • 硬件依赖——代码直接操作寄存器、GPIO、ADC,测试时硬件可能还没做出来
  • 实时性约束——中断、定时器、任务调度,这些在测试环境里根本模拟不了
  • 资源受限——没有文件系统、没有标准库、甚至没有printf

说白了,你写了个UART发送函数,想测它能不能正确处理数据。但测试环境里没有UART硬件,怎么办?这时候就需要桩函数——它假装自己是硬件,替你完成交互验证。

核心观点:桩函数(Stub)模拟被调用模块的行为,驱动(Driver)模拟调用者环境。两者配合,才能把嵌入式代码从硬件中剥离出来单独测试。

一个真实案例

我记得有一次,团队开发一个温度采集模块。代码逻辑是这样的:

// 温度采集模块
float read_temperature(void) {
    uint16_t raw = read_adc_channel(3);  // 依赖ADC驱动
    return (raw * 3.3f / 4096.0f - 0.5f) / 0.01f;
}

这个函数依赖 read_adc_channel(),而ADC驱动当时还在调试。没有桩函数,我们根本没法测温度转换逻辑对不对。后来我写了个简单的桩:

// 桩函数:模拟ADC读取
uint16_t read_adc_channel(uint8_t channel) {
    // 返回固定值,模拟不同温度
    static const uint16_t test_values[] = {2048, 1024, 512};
    static int index = 0;
    return test_values[index++ % 3];
}

配合驱动代码调用这个桩,我们很快就发现了温度计算公式里的一个符号错误。嗯,要是等硬件出来再测,至少耽误两周。

桩函数 vs 驱动:到底谁是谁?

很多初学者搞不清这两个概念。我画个图你就明白了:

桩函数与驱动的关系 测试框架 驱动(Driver) 被测模块(SUT) 桩函数(Stub) 关键区别 驱动:主动调用被测模块,提供测试输入 桩函数:被动响应被测模块的调用,模拟底层依赖

我的经验:写桩函数时,别想着一次把所有情况都模拟全。先写最简单的固定返回值桩,跑通基本路径,再逐步增加复杂行为。我曾经一上来就写了个带状态机的桩,结果桩本身比被测代码还复杂,调试了两天才发现是桩写错了。

什么时候必须用桩函数?

不是所有测试都需要桩。我总结了几种必须用的情况:

场景 原因 典型例子
硬件未就绪 硬件还在设计/调试阶段 ADC、DAC、Flash驱动
外部依赖不稳定 依赖的模块经常变更 通信协议栈、文件系统
异常场景难复现 硬件故障、超时、数据异常 看门狗超时、CRC校验失败
实时性要求高 测试环境无法模拟真实时序 中断处理、RTOS任务切换

避坑指南:我曾经在一个项目中,为了省事直接用了真实硬件做单元测试。结果每次跑测试都要插拔开发板,而且硬件偶尔会出莫名其妙的问题,导致测试结果不可复现。后来老老实实写了桩函数,测试效率提升了至少3倍。

驱动开发:不只是"调用一下"

驱动(Driver)是测试框架和被测模块之间的桥梁。它负责:

  • 初始化测试环境
  • 构造测试输入数据
  • 调用被测函数
  • 收集并验证输出结果

一个典型的驱动代码长这样:

// 驱动:测试温度采集模块
void test_read_temperature(void) {
    // 1. 准备测试数据
    float expected[] = {25.0f, 0.0f, -10.0f};
    
    // 2. 调用被测函数
    float result = read_temperature();
    
    // 3. 验证结果
    if (abs(result - expected[0]) < 0.1f) {
        printf("PASS: temperature = %.2f\n", result);
    } else {
        printf("FAIL: expected %.2f, got %.2f\n", expected[0], result);
    }
}

你可能会问:这不就是个简单的调用吗?其实不然。好的驱动设计要考虑测试覆盖率、边界条件、异常注入。我习惯把驱动和桩函数放在同一个测试文件中,方便维护。

本章小结

桩函数和驱动是嵌入式单元测试的基石。没有它们,你就是在跟硬件搏斗,而不是在测代码逻辑。记住三点:

  1. 桩函数模拟依赖——让被测模块脱离硬件独立运行
  2. 驱动提供环境——构造输入、调用函数、验证输出
  3. 两者配合——才能实现真正的自动化单元测试