桩函数与驱动函数:集成测试的两大基石

做嵌入式集成测试,你绕不开两个东西——桩函数和驱动函数。说白了,它们就是测试环境里的「替身」和「导演」。我刚开始带项目时,团队里有人觉得写桩和驱动是浪费时间,结果模块联调时问题一堆,定位花了三倍时间。嗯,从那以后,我再也不敢小看这两个基础概念了。

一句话记住:驱动函数是「调用者」,桩函数是「被调用者」。测试时,驱动主动发起调用,桩被动响应并返回预设结果。

桩函数(Stub)定义与作用

桩函数,就是替身。当被测模块需要调用某个下层函数,但那个函数还没写好、或者不方便在测试环境里跑(比如硬件相关、网络通信),我们就写一个简化版替上去。

它的核心作用有三个:

  • 隔离依赖——让被测模块不依赖真实的下层实现
  • 控制返回值——可以预设正常值、边界值、异常值
  • 记录调用行为——检查被测模块是否真的调用了、传参对不对

我在项目中遇到过最典型的场景:一个传感器驱动还没调通,但上层算法模块要测。我就写了个桩函数,每次返回固定温度值25.0。算法模块跑通了,等传感器驱动就绪后直接替换桩,一次通过。

我的习惯:桩函数不要写太复杂。够用就行,能返回预设值、能记录调用次数和参数,就够了。写复杂了反而容易引入新bug。

驱动函数(Driver)定义与作用

驱动函数,就是测试的发起者。它负责调用被测模块的接口,传入测试数据,收集结果。你可以把它理解成一个「临时主函数」。

它的核心作用:

  • 构造测试场景——初始化被测模块,准备输入数据
  • 执行测试用例——按顺序调用被测接口
  • 收集并判断结果——比对实际输出和预期值

你想想看,没有驱动函数,你怎么让被测模块跑起来?总不能每次都改main函数吧。驱动函数就是给被测模块搭了个「测试舞台」。

桩与驱动的调用关系

它们的关系其实很清晰。我画了一张图,你看一眼就明白了:

桩函数与驱动函数调用关系图 驱动函数 (Driver) 被测模块 (UT) 桩函数 A (Stub) 桩函数 B (Stub) 桩函数 C (Stub) 调用 调用 调用 调用 驱动主动发起 → 被测模块执行 → 桩函数返回预设结果 → 驱动收集并判断

流程很简单:

  1. 驱动函数调用被测模块的接口
  2. 被测模块执行过程中,遇到下层依赖,调用桩函数
  3. 桩函数返回预设值,被测模块继续执行
  4. 驱动函数收集被测模块的输出,与预期值比对

我曾经踩过的坑:桩函数返回值和真实模块不一致。比如真实函数返回的是int,桩函数返回了float,类型隐式转换导致被测模块行为异常。排查了两天才发现。所以写桩时,函数签名一定要和原函数完全一致。

典型应用场景分析

光说概念太虚,我列几个实际项目中常见的场景:

场景 驱动做什么 桩做什么
硬件尚未就绪 调用传感器数据采集接口 返回预设的传感器值(温度、压力等)
外部通信模块缺失 调用数据发送接口 模拟接收方,返回ACK或超时
异常路径覆盖 传入非法参数 返回错误码或抛出异常
时序/并发测试 按特定时序调用多个接口 模拟不同响应延迟
第三方库未集成 调用加密/校验接口 返回固定校验值,不依赖真实库

我印象最深的是做车载ECU测试那次。CAN通信模块的硬件还在调试,但上层应用要测。我写了三个桩函数:一个模拟发送成功、一个模拟总线繁忙、一个模拟超时。驱动函数循环调用这三种场景,把应用层的异常处理逻辑测了个遍。等硬件就绪后,桩函数直接替换成真实驱动,零改动通过。

一个小技巧:写桩函数时,可以加一个「调用计数器」和「参数记录器」。这样驱动函数在测试结束后,可以检查被测模块是否按预期调用了桩函数、传参是否正确。这个习惯帮我抓到了不少隐蔽的调用顺序bug。

桩与驱动的代码骨架

光说不练假把式。我写个最简单的例子,你感受一下结构:

/* 桩函数:模拟底层传感器读取 */
int Stub_ReadSensor(void)
{
    static int callCount = 0;
    callCount++;
    printf("[Stub] ReadSensor 被调用,次数:%d\n", callCount);
    return 25;  /* 固定返回25度 */
}

/* 驱动函数:测试上层温度处理模块 */
void Driver_TestTempProcess(void)
{
    int result;
    int expected = 25;

    /* 初始化被测模块 */
    TempProcess_Init();

    /* 执行测试 */
    result = TempProcess_GetTemperature();

    /* 判断结果 */
    if (result == expected) {
        printf("[Driver] 测试通过,结果=%d\n", result);
    } else {
        printf("[Driver] 测试失败,期望=%d,实际=%d\n", expected, result);
    }
}

这个例子虽然简单,但骨架是通用的。驱动函数负责「搭台」,桩函数负责「唱戏」,被测模块在中间「演」。你只要把这个模式套到自己的项目里,集成测试就能快速跑起来。

核心要点回顾:

  • 驱动函数是测试的发起者,负责调用和判断
  • 桩函数是依赖的替身,负责返回预设结果
  • 驱动和桩配合,让被测模块在隔离环境中被充分测试
  • 写桩时注意函数签名一致,写驱动时注意场景覆盖