2. 测试桩理论基础:桩函数定义、桩函数分类

各位同学,咱们今天聊聊测试桩的理论基础。说实话,很多刚入行的嵌入式工程师,一听到“桩”就头大。其实没那么玄乎。

我当年第一次做集成测试,面对一个几十万行的通信协议栈,根本不知道从哪下手。后来老工程师丢给我一句话:“先把桩打好,再谈测试。” 这句话我记到现在。

2.1 桩函数到底是个啥?

桩函数,英文叫 Stub Function。说白了,就是一个“替身演员”。

在集成测试中,我们测试模块A,但A要调用模块B的函数。如果B还没写好,或者B依赖硬件(比如ADC、SPI),怎么办?

答案就是:写一个假的B函数,让它返回我们想要的值。这个假的函数,就是桩。

桩函数的本质: 用轻量级的模拟实现,替换真实依赖,让被测模块能独立运行。

举个例子,你测试一个温度采集模块,它调用了 HAL_ADC_GetValue()。真实情况下,这个函数会去读ADC寄存器。但在测试环境里,你写个桩:

// 真实函数(硬件相关)
uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef* hadc) {
    // 读取硬件寄存器...
    return reg_value;
}

// 桩函数(测试用)
uint32_t HAL_ADC_GetValue_Stub(ADC_HandleTypeDef* hadc) {
    // 直接返回预设值
    return 0x0FFF;  // 模拟满量程
}

你看,桩函数把硬件依赖切断了。这样你就能在PC上跑测试,不用每次都烧录到板子上。

我的习惯: 桩函数的命名,我一般会在原函数名后面加 _Stub 后缀。这样一看就知道是测试用的,不会和正式代码混淆。

2.2 桩函数的三种分类

桩函数不是千篇一律的。根据用途和复杂度,我习惯把它们分成三类:哑元、伪桩、智能桩。

这个分类方法,是我在做一个汽车电子项目时总结出来的。当时团队里七八个人,每个人写的桩风格都不一样,集成的时候乱成一锅粥。后来我定了这个分类标准,大家照着做,效率明显上来了。

分类 复杂度 典型用途 返回值控制 行为记录
哑元 (Dummy) 最低 占位、编译通过 固定值或空 无
伪桩 (Fake) 中等 模拟简单行为 可配置 有限
智能桩 (Mock) 最高 验证交互逻辑 动态响应 完整调用记录

2.3 哑元(Dummy)—— 最朴素的桩

哑元,就是最基础的桩。它的任务只有一个:让代码能编译通过。

什么时候用哑元?比如你的模块里调了一个打印函数 DEBUG_LOG(),但在测试环境里你不想真的打印。那就写个空函数:

// 哑元:什么都不做
void DEBUG_LOG_Stub(const char* msg) {
    // 空函数体,编译通过即可
    return;
}

哑元不关心参数,不返回有意义的值(或者返回一个固定值)。它就是个“空气桩”。

注意: 哑元虽然简单,但有个坑。如果被测代码依赖桩函数的返回值做判断,哑元返回固定值可能导致测试结果失真。我曾经在一个项目中,哑元返回了0,结果被测模块以为“操作成功”,一路执行下去,最后崩了。排查了半天才发现是桩的问题。

2.4 伪桩(Fake)—— 带点“脑子”的桩

伪桩比哑元高级一点。它能模拟真实函数的部分行为,比如返回一个可配置的值,或者维护一个简单的内部状态。

伪桩的核心是:可控。你可以在测试用例里设置它返回什么。

// 伪桩:可配置返回值
static int fake_return_value = 0;

void set_fake_return_value(int val) {
    fake_return_value = val;
}

int GetSensorValue_Stub(void) {
    return fake_return_value;  // 返回测试用例设置的值
}

这样,你的测试用例就可以这样写:

// 测试用例:传感器超限报警
void test_sensor_alarm(void) {
    set_fake_return_value(150);  // 设置温度150度(超限)
    int result = CheckTemperature();
    assert(result == ALARM_TRIGGERED);
}

伪桩很适合模拟那些“返回值有规律”的函数,比如读取传感器、获取时间戳等。

我的经验: 伪桩里我习惯加一个 call_count 计数器,记录被调用了多少次。这样能发现“函数被多调了一次”或者“根本没调用”的bug。这个习惯帮我抓出过好几个隐蔽的时序问题。

2.5 智能桩(Mock)—— 最强大的桩

智能桩,也叫Mock对象。它不仅能返回值,还能验证调用行为。

智能桩会记录:

  • 被调用了多少次?
  • 每次传入的参数是什么?
  • 调用顺序对不对?

举个例子,你测试一个“发送数据包”的函数,它内部会调用 UART_SendByte() 三次。智能桩可以验证:

// 智能桩:记录调用信息
typedef struct {
    int call_count;
    uint8_t last_byte;
    uint8_t all_bytes[10];
} UART_Mock;

static UART_Mock mock = {0};

void UART_SendByte_Stub(uint8_t data) {
    mock.call_count++;
    mock.last_byte = data;
    if (mock.call_count <= 10) {
        mock.all_bytes[mock.call_count - 1] = data;
    }
}

// 验证函数
int UART_SendByte_GetCallCount(void) {
    return mock.call_count;
}

uint8_t UART_SendByte_GetLastByte(void) {
    return mock.last_byte;
}

测试用例就可以做更精细的验证:

void test_send_packet(void) {
    SendPacket(0xAA, 0xBB, 0xCC);
    
    assert(UART_SendByte_GetCallCount() == 3);
    assert(UART_SendByte_GetLastByte() == 0xCC);
    // 还可以验证发送顺序:0xAA, 0xBB, 0xCC
}

智能桩的威力在于:它让你能看见模块内部的调用细节。这在调试复杂的状态机、协议栈时特别有用。

核心区别一句话: 哑元只管“存在”,伪桩管“返回值”,智能桩管“行为验证”。

2.6 三种桩的选择策略

你可能会问:那我到底该用哪种?

我的建议是:够用就好,不要过度设计。

  • 日志、调试打印 → 哑元,够用
  • 传感器读取、状态查询 → 伪桩,可控就行
  • 通信协议、状态机、回调函数 → 智能桩,必须验证交互

我曾经见过一个团队,所有桩都用智能桩,每个桩函数写了几十行。结果测试代码比被测代码还复杂,维护成本飙升。没必要。

避坑指南: 智能桩虽然强大,但不要滥用。如果一个函数只是“返回一个值”,用伪桩就够了。智能桩适合验证“调用关系”和“交互顺序”,而不是返回值本身。

2.7 本章知识体系

下面这张图,把三种桩的关系和适用场景梳理了一下。你可以把它当作一个快速参考。

测试桩分类与选择流程图 测试桩分类与选择策略 测试桩 (Stub) 哑元 (Dummy) 仅占位,编译通过 伪桩 (Fake) 可配置返回值 智能桩 (Mock) 验证调用行为 适用场景 日志打印、调试输出 无返回值的回调 适用场景 传感器读取、状态查询 时间戳、随机数 适用场景 通信协议、状态机 回调函数、中断处理 选择原则:够用就好,不要过度设计 哑元 → 伪桩 → 智能桩,按需升级

嗯,这一章的内容就到这里。三种桩的概念搞清楚了,后面写桩函数的时候,你就能根据实际情况,选最合适的类型。不会一上来就写个几百行的智能桩,也不会该验证的时候只用了个哑元。