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

说到模拟依赖项,就不得不提两个概念:StubMock。很多人搞混它们,我简单说说我的理解。

类型 作用 我常用的场景
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。它记录了被调用的次数,我们通过断言来验证行为是否符合预期。

我曾经踩过的坑: 有一次我写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状态

嗯,组件测试这块内容其实不难,但很实用。你想想看,如果每个组件都能单独验证,最后集成的时候心里就有底多了。下一讲我们会聊聊集成测试,到时候这些组件就要“手拉手”一起跑了。到时候你就知道,组件测试做得扎实,集成测试会省多少力气。