——测试替身(Test Double)全解:Dummy、Fake、Spy与Mock的区别与应用)
❄️ 我的个人专栏《智能软件工程AI4SE》《嵌入式面试总结》《嵌入式处理器架构解析》《嵌入式与虚拟化》《嵌入式软件测试》 Simplicity is the ultimate sophistication摘要本文系统讲解嵌入式软件动态测试中的四类测试替身——Dummy、Fake、Spy 与 Mock。Dummy 仅用于占位Fake 提供简化但可用的真实实现Spy 记录调用供事后断言Mock 预设期望并实时验证交互。文章通过 C 代码示例逐一演示四者的典型用法并结合硬件抽象层设计、链接期替换、中断并发等嵌入式场景给出选型与实践建议帮助读者写出稳定且可维护的单元测试。1. 引言在嵌入式软件动态测试中被测单元往往依赖外部硬件、传感器、通信总线或底层驱动。为了隔离这些依赖、让测试稳定且可重复测试替身Test Double成为最常用的手段之一。本文围绕 Dummy、Fake、Spy 与 Mock 四类测试替身展开说明它们的定义、适用场景、区别以及嵌入式环境下的实践要点。2. 什么是测试替身测试替身是指用一个可控的替身对象或替身模块替代被测单元所依赖的真实组件从而让测试聚焦于被测单元自身的逻辑。测试替身并非测试框架专属概念在嵌入式开发中它既可以表现为软件层面的桩函数也可以表现为硬件抽象层HAL的模拟实现。引入测试替身的主要目的包括隔离外部依赖避免真实硬件、总线或外设的不确定性影响测试结果。加速测试执行用轻量替身替代慢速或昂贵的真实设备。构造边界场景模拟故障、超时、异常数据等难以在真实环境复现的情况。提升可重复性让同一用例在不同机器、不同时间得到一致结果。3. 四类测试替身概览测试替身通常按用途和验证方式分为四类下面先给出整体对比再逐一展开。类型核心作用是否验证调用典型嵌入式场景Dummy仅占位不参与逻辑否填充函数参数、满足接口签名Fake提供简化但可用的真实实现否内存文件系统、环形缓冲区模拟Spy记录调用信息事后检查是事后断言记录传感器读取次数、记录日志输出Mock预设期望验证交互是预先设定验证驱动初始化顺序、验证中断注册4. Dummy只占位不做事Dummy 是最简单的测试替身它只用于满足函数签名或参数列表本身不包含任何业务逻辑也不会被真正调用。如果被测代码在某个分支中根本不会使用该参数那么传入 Dummy 就足够了。典型场景被测函数要求传入一个回调指针或配置结构体但当前测试路径并不触发该回调此时可以传入一个空结构体或空指针占位。/* Dummy仅用于占位不会被调用 */ static void dummy_callback(uint32_t event) { (void)event; } void test_sensor_init_with_dummy(void) { sensor_config_t cfg {0}; /* 传入 dummy 回调满足接口签名 */ sensor_init(cfg, dummy_callback); /* 断言初始化结果 */ TEST_ASSERT_EQUAL(SENSOR_OK, sensor_get_status()); }使用 Dummy 时要注意它不验证任何行为也不提供返回值逻辑因此只适合那些确实不会被当前测试路径触发的参数。5. Fake简化但可用的真实实现Fake 提供一个简化版本的真实实现行为与真实组件在逻辑上一致但去掉了硬件依赖或复杂细节。Fake 通常有真实的状态和返回值可以被被测代码正常调用。典型场景用内存数组模拟 EEPROM 存储用环形缓冲区模拟串口收发用软件定时器模拟硬件定时器。/* Fake用内存数组模拟 EEPROM */ static uint8_t fake_eeprom[256]; int fake_eeprom_write(uint16_t addr, const uint8_t *data, uint16_t len) { if (addr len sizeof(fake_eeprom)) { return -1; } memcpy(fake_eeprom[addr], data, len); return 0; } int fake_eeprom_read(uint16_t addr, uint8_t *data, uint16_t len) { if (addr len sizeof(fake_eeprom)) { return -1; } memcpy(data, fake_eeprom[addr], len); return 0; }Fake 的价值在于让被测代码走通完整逻辑路径同时避免依赖真实硬件。它不记录调用也不预设期望只提供可用的行为。6. Spy记录调用事后检查Spy 在真实实现或简化实现的基础上额外记录调用信息例如调用次数、传入参数、调用顺序等。测试结束后通过断言这些记录来验证被测代码的行为。典型场景验证某个传感器在超时后是否被重新读取验证日志模块是否输出了预期条数。/* Spy记录调用次数和参数 */ static int spi_transfer_count; static uint8_t spi_last_tx; int spy_spi_transfer(uint8_t tx, uint8_t *rx) { spi_transfer_count; spi_last_tx tx; *rx 0xAA; /* 返回固定值 */ return 0; } void test_spi_send_increments_count(void) { spi_transfer_count 0; driver_send_byte(0x55); TEST_ASSERT_EQUAL(1, spi_transfer_count); TEST_ASSERT_EQUAL(0x55, spi_last_tx); }Spy 与 Mock 的关键区别在于Spy 使用真实或简化实现调用后由测试代码检查记录Mock 则在调用前就预设了期望调用时自动校验。7. Mock预设期望验证交互Mock 在测试开始前就设定好期望的调用序列、参数和返回值。被测代码执行时Mock 会实时校验实际调用是否与期望一致一旦出现偏差立即失败。典型场景验证驱动初始化时是否按正确顺序调用底层 HAL验证中断处理是否在正确时机注册回调。/* Mock预设期望并验证调用顺序 */ void test_driver_init_calls_hal_in_order(void) { /* 预设期望先打开时钟再配置引脚最后使能中断 */ mock_expect(hal_clock_enable); mock_expect(hal_gpio_config); mock_expect(hal_irq_enable); driver_init(); /* 所有期望均已满足测试通过 */ mock_verify(); }Mock 适合验证「被测单元是否以正确方式与外部组件交互」而 Fake 更适合验证「被测单元在给定输入下是否产生正确输出」。8. 四者对比与选型建议在实际项目中四类测试替身并非互斥常常组合使用。选型时可以参考以下原则只满足签名、不参与逻辑用 Dummy。需要真实行为但不想依赖硬件用 Fake。需要事后确认调用情况用 Spy。需要预先约束交互方式用 Mock。一个常见误区是「所有替身都叫 Mock」。实际上 Mock 只是测试替身的一种过度使用 Mock 会让测试与实现细节耦合过紧反而降低测试的可维护性。建议优先使用 Fake 覆盖正常逻辑路径仅在需要验证交互顺序或调用关系时引入 Mock 或 Spy。9. 嵌入式环境下的实践要点嵌入式动态测试引入测试替身时需要额外关注以下几点硬件抽象层设计提前把硬件操作封装到 HAL 接口后面便于在测试中替换为 Fake 或 Mock。链接期替换在单元测试构建时通过链接器将真实驱动符号替换为测试替身符号避免修改被测源码。中断与并发涉及中断的测试替身要谨慎处理时序必要时用标志位或队列模拟异步行为。资源受限Fake 的内存占用要可控避免在资源紧张的 MCU 上引入过大替身结构。回归风险替身行为与真实硬件不一致时可能导致测试通过但真机失败因此关键路径仍需保留硬件在环测试。10. 总结测试替身是嵌入式软件动态测试中隔离外部依赖、提升测试稳定性的核心手段。Dummy 负责占位Fake 提供简化实现Spy 记录调用供事后检查Mock 预设期望并实时验证交互。理解四者的区别并结合硬件抽象层合理选型才能写出既稳定又有价值的嵌入式单元测试。