4、组件测试:ESP-IDF组件结构、组件级测试方法、模拟依赖项(Mocks与Stubs)
好,咱们进入第四讲。组件测试,说白了就是把你ESP-IDF项目里的一个个组件拆出来,单独测。
我刚开始做嵌入式测试那会儿,习惯把整个固件烧进去跑。后来发现一个问题——出了问题,你根本不知道是哪个模块的锅。是Wi-Fi驱动挂了?还是我的业务逻辑写崩了?排查起来特别痛苦。
所以,组件测试的价值就在这里。它让你能聚焦到一个小单元,快速验证它的行为对不对。
4.1 ESP-IDF的组件结构
先看看ESP-IDF里组件长什么样。你打开一个ESP-IDF项目,通常会看到这样的目录结构:
my_project/
├── CMakeLists.txt
├── main/
│ ├── CMakeLists.txt
│ └── main.c
└── components/
├── sensor_driver/
│ ├── CMakeLists.txt
│ ├── sensor_driver.c
│ └── include/
│ └── sensor_driver.h
└── network_manager/
├── CMakeLists.txt
├── network_manager.c
└── include/
└── network_manager.h
每个组件都有自己的CMakeLists.txt。这个文件定义了组件的源文件、头文件路径、依赖关系。我个人习惯把功能相对独立的模块都做成组件,比如传感器驱动、网络管理、数据存储等等。
组件之间可以互相依赖。比如network_manager可能依赖sensor_driver来获取数据再上传。但测试的时候,我们不想真的去操作传感器硬件,对吧?这就引出了模拟依赖项的概念。
4.2 组件级测试方法
组件测试,核心思路是:只测当前组件,不测它的依赖。
举个例子。你写了一个温度报警组件,它从传感器读取温度,如果超过阈值就触发报警。测试这个组件时,你关心的是:
- 温度低于阈值时,不报警
- 温度高于阈值时,报警
- 阈值设置是否正确
你并不关心传感器硬件是否真的能读到温度。所以,我们需要把传感器这个依赖项“替换掉”。
在ESP-IDF里做组件测试,我推荐用Unity测试框架。它轻量、适合嵌入式。你可以在组件的test目录下写测试用例:
components/
└── temp_alarm/
├── CMakeLists.txt
├── temp_alarm.c
├── include/
│ └── temp_alarm.h
└── test/
├── CMakeLists.txt
└── test_temp_alarm.c
测试文件里,你可以这样写:
#include "unity.h"
#include "temp_alarm.h"
// 模拟的传感器读取函数
static int mock_temperature = 0;
int sensor_read_temperature(void) {
return mock_temperature;
}
void setUp(void) {
mock_temperature = 25; // 默认室温
}
void tearDown(void) {
// 清理工作
}
void test_temperature_below_threshold_should_not_alarm(void) {
mock_temperature = 30;
temp_alarm_set_threshold(50);
TEST_ASSERT_FALSE(temp_alarm_check());
}
void test_temperature_above_threshold_should_alarm(void) {
mock_temperature = 60;
temp_alarm_set_threshold(50);
TEST_ASSERT_TRUE(temp_alarm_check());
}
你看,我们并没有真的去调用硬件API,而是自己写了一个sensor_read_temperature函数,返回我们想要的值。这就是组件测试的精髓。
4.3 模拟依赖项:Mocks与Stubs
说到模拟依赖项,就不得不提两个概念:Stub和Mock。很多人搞混它们,我简单说说我的理解。
| 类型 | 作用 | 我常用的场景 |
|---|---|---|
| Stub | 替换真实函数,返回预设值 | 传感器读取、GPIO状态获取 |
| Mock | 替换真实函数,并验证调用行为 | 检查某个函数是否被调用、调用次数、参数是否正确 |
Stub很简单。就像上面那个例子,我们写了一个sensor_read_temperature,返回mock_temperature。它不关心谁调用了它,也不关心调了几次。它只是“站在那里”,返回你给的值。
Mock则更“聪明”一些。它不仅能返回预设值,还能记录调用信息。比如,你想验证“报警触发后,是否调用了蜂鸣器驱动”。你可以这样写:
// 模拟的蜂鸣器驱动
static int buzzer_on_call_count = 0;
void buzzer_on(void) {
buzzer_on_call_count++;
}
void test_alarm_triggered_should_turn_on_buzzer(void) {
mock_temperature = 60;
temp_alarm_set_threshold(50);
temp_alarm_check();
// 验证蜂鸣器被调用了1次
TEST_ASSERT_EQUAL(1, buzzer_on_call_count);
}
这里buzzer_on就是一个Mock。它记录了被调用的次数,我们通过断言来验证行为是否符合预期。
setUp函数里重置调用计数。结果上一个测试用例调了3次,下一个用例一上来就断言等于1,死活过不去。排查了半天才发现是计数没清零。所以,每个测试用例的setUp一定要把Mock的状态重置干净。
4.4 在ESP-IDF中配置测试
要让测试跑起来,你需要在组件的CMakeLists.txt里加上测试相关的配置。我一般这样写:
idf_component_register(SRCS "temp_alarm.c"
INCLUDE_DIRS "include"
REQUIRES driver)
# 如果启用了测试
if(CONFIG_TEMP_ALARM_TESTS)
idf_component_register(SRCS "test/test_temp_alarm.c"
INCLUDE_DIRS "include"
REQUIRES unity)
endif()
然后在项目根目录运行:
idf.py -T temp_alarm test
这样ESP-IDF的测试框架就会自动编译并运行你的组件测试。我个人习惯在持续集成(CI)里加上这个命令,每次提交代码都自动跑一遍组件测试,确保没有引入回归问题。
核心要点总结:
- 组件测试只测当前组件,不依赖真实硬件
- 用Stub替换数据来源,返回预设值
- 用Mock验证行为,检查调用次数和参数
- 每个测试用例只验证一个逻辑分支
- 别忘了在
setUp里重置Mock状态
嗯,组件测试这块内容其实不难,但很实用。你想想看,如果每个组件都能单独验证,最后集成的时候心里就有底多了。下一讲我们会聊聊集成测试,到时候这些组件就要“手拉手”一起跑了。到时候你就知道,组件测试做得扎实,集成测试会省多少力气。