ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

嵌入式软件动态测试(七)——Mock与Stub技术:隔离外部依赖的单元测试设计模式

嵌入式软件动态测试(七)——Mock与Stub技术:隔离外部依赖的单元测试设计模式 ❄️ 我的个人专栏《智能软件工程AI4SE》《嵌入式面试总结》《嵌入式处理器架构解析》《嵌入式与虚拟化》《嵌入式软件测试》 Simplicity is the ultimate sophistication摘要本文介绍嵌入式软件单元测试中隔离外部依赖的 Mock 与 Stub 技术。文章先说明隔离外部依赖的必要性再对比 Stub 与 Mock 的核心概念与区别随后介绍链接期替换、函数指针注入、预处理宏替换和硬件抽象层隔离四种嵌入式实现方式并通过 C 语言示例演示 Stub 与 Mock 的实际用法最后总结常见陷阱与注意事项。1. 引言在嵌入式软件单元测试中被测函数往往依赖硬件寄存器、外设驱动、操作系统接口或通信协议栈等外部组件。这些依赖在测试环境中可能不可用、行为不确定或代价高昂导致测试无法稳定运行。Mock 与 Stub 技术正是为了解决这一问题而诞生的测试设计模式它们通过隔离外部依赖让被测单元在可控、可观测的环境中独立验证。2. 为什么需要隔离外部依赖嵌入式系统的软件层次通常紧贴硬件被测函数与外部依赖之间存在强耦合。这种耦合在单元测试阶段会带来三类典型问题环境不可用传感器、通信总线或外部存储设备在开发机上不存在测试无法直接调用真实驱动。行为不确定硬件中断时序、网络延迟或随机噪声会导致测试结果不稳定难以复现缺陷。代价高昂依赖真实硬件或外设的测试执行慢、成本高不适合频繁回归。通过引入 Mock 与 Stub测试代码可以替代真实依赖将测试焦点收敛到被测单元自身的逻辑正确性上。3. Stub 与 Mock 的核心概念Stub 与 Mock 常被混用但二者在测试设计模式中有明确分工。3.1 Stub预设返回值的替身Stub 是一个替身对象它按照测试需要预先设定返回值或行为用于让被测单元沿着特定路径执行。Stub 只关心“给什么结果”不验证被测单元是否以预期方式调用了它。3.2 Mock验证交互行为的替身Mock 不仅提供预设行为还会记录调用次数、参数顺序和调用时机并在测试结束时断言这些交互是否符合预期。Mock 关心的是“被测单元是否正确地使用了依赖”。3.3 二者的区别维度StubMock核心目的提供预设返回值驱动被测路径验证调用交互是否符合预期是否断言调用通常不断言必须断言调用次数与参数适用场景依赖只提供数据输入依赖的调用顺序和频率是关键逻辑4. 嵌入式环境中的 Mock 与 Stub 实现方式嵌入式开发中根据目标平台和工具链的不同常用的实现方式有以下几种。4.1 链接期替换在编译链接阶段将真实依赖的符号替换为测试桩函数。这种方式无需修改被测源码适合替换驱动函数和操作系统接口。4.2 函数指针注入将被测模块依赖的外部函数封装为函数指针并在测试初始化时注入替身实现。这种方式适合模块化设计较好的代码。4.3 预处理宏替换通过宏定义将真实函数名映射到测试替身适用于无法修改接口签名的遗留代码。4.4 硬件抽象层隔离在驱动层之上建立硬件抽象层测试时替换为模拟实现。这是嵌入式单元测试中最推荐的长期方案。5. 代码示例链接期替换实现 Stub下面以 C 语言为例演示如何通过链接期替换隔离温度传感器驱动。/* 被测模块 temperature_monitor.c */ #include temperature_monitor.h #include sensor_driver.h float read_temperature_celsius(void) { uint16_t raw sensor_read_raw(); return (float)raw * 0.01f; }/* 测试桩 sensor_driver_stub.c */ #include sensor_driver.h static uint16_t stub_raw_value 2500u; void sensor_driver_set_raw(uint16_t value) { stub_raw_value value; } uint16_t sensor_read_raw(void) { return stub_raw_value; }/* 单元测试 test_temperature_monitor.c */ #include unity.h #include temperature_monitor.h #include sensor_driver_stub.h void setUp(void) { sensor_driver_set_raw(2500u); } void test_read_temperature_celsius(void) { TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.0f, read_temperature_celsius()); } void test_raw_value_zero(void) { sensor_driver_set_raw(0u); TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.0f, read_temperature_celsius()); }链接时将测试工程中的sensor_driver_stub.c与temperature_monitor.c一起编译真实驱动不参与链接即可实现隔离。6. 代码示例函数指针注入实现 Mock当被测模块通过函数指针调用外部依赖时可以在测试中注入 Mock 并验证交互行为。/* 被测模块 data_logger.c */ #include data_logger.h static int (*send_func)(const uint8_t *data, uint16_t len) NULL; void data_logger_set_sender(int (*func)(const uint8_t *, uint16_t)) { send_func func; } int log_data(const uint8_t *data, uint16_t len) { if (send_func NULL) { return -1; } return send_func(data, len); }/* 测试文件 test_data_logger.c */ #include unity.h #include data_logger.h static int call_count 0; static uint16_t last_len 0; static int mock_send(const uint8_t *data, uint16_t len) { call_count; last_len len; return 0; } void setUp(void) { call_count 0; last_len 0; data_logger_set_sender(mock_send); } void test_log_data_calls_sender_once(void) { uint8_t buf[4] {1, 2, 3, 4}; TEST_ASSERT_EQUAL_INT(0, log_data(buf, 4)); TEST_ASSERT_EQUAL_INT(1, call_count); TEST_ASSERT_EQUAL_UINT16(4, last_len); } void test_log_data_without_sender(void) { data_logger_set_sender(NULL); TEST_ASSERT_EQUAL_INT(-1, log_data(NULL, 0)); TEST_ASSERT_EQUAL_INT(0, call_count); }通过断言call_count和last_len测试验证了被测单元确实以正确的参数调用了依赖这正是 Mock 的核心价值。7. 常见陷阱与注意事项过度 Mock 导致测试失真如果替身行为与真实依赖差异过大测试可能掩盖集成缺陷应尽量保持替身行为与真实接口一致。忽略返回值边界Stub 应覆盖正常值、边界值和异常值避免只测一条快乐路径。链接期替换的符号冲突多个测试文件同时定义同名桩函数会导致链接错误建议按模块划分桩文件。函数指针注入的初始化遗漏每个测试用例的setUp中都要重新注入替身防止用例间状态泄漏。硬件抽象层设计滞后尽早建立硬件抽象层避免后期大规模重构。8. 总结Mock 与 Stub 是嵌入式单元测试中隔离外部依赖的两种互补设计模式。Stub 提供预设行为以驱动被测路径Mock 进一步验证交互是否符合预期。链接期替换、函数指针注入、预处理宏替换和硬件抽象层隔离是嵌入式环境中常用的实现手段。合理运用这些技术可以显著提升单元测试的稳定性、可重复性和缺陷定位效率。
返回列表