4. 汽车电子典型环境应力
单元测试,说白了就是给函数做“体检”。你写了一个模块,它能不能跑?边界条件处理了没?异常输入会不会崩?这些不能靠拍脑袋,得靠工具。
嵌入式 C 语言的单元测试,跟桌面端不太一样。资源受限、硬件依赖多、交叉编译环境复杂。我这些年用过不少框架,最后常驻工具箱的是三个:CUnit、Unity、CMock。它们各有脾气,选对了能省一半力气。
4.1 三大框架的定位与选型
先看一张对比表,帮你快速建立印象:
| 框架 | 核心特点 | 适用场景 | 资源开销 | 个人评价 |
|---|---|---|---|---|
| CUnit | 类 JUnit 风格,测试注册 + 运行器 | PC 端模拟测试、Linux 环境 | 中等,需动态内存 | 成熟稳定,但太重,不适合裸机 |
| Unity | 纯 C 宏驱动,无动态分配 | 嵌入式目标板、资源受限 MCU | 极低,ROM/RAM 友好 | 我的首选,裸机项目几乎都用它 |
| CMock | 自动生成 Mock 桩代码 | 需要隔离硬件依赖的测试 | 生成代码较大,需配合 Unity | 好用,但别滥用,否则测试比代码还长 |
4.2 CUnit:桌面模拟测试的老将
CUnit 的结构很清晰:你写测试函数,注册到 Suite 里,然后跑。它提供了断言宏、测试运行器、结果输出。
一个典型例子:
#include <CUnit/CUnit.h>
#include <CUnit/Basic.h>
/* 被测函数 */
int add(int a, int b) {
return a + b;
}
/* 测试用例 */
void test_add_positive(void) {
CU_ASSERT(add(2, 3) == 5);
}
void test_add_negative(void) {
CU_ASSERT(add(-1, -2) == -3);
}
int main() {
CU_initialize_registry();
CU_pSuite suite = CU_add_suite("Suite_Add", NULL, NULL);
CU_add_test(suite, "test positive", test_add_positive);
CU_add_test(suite, "test negative", test_add_negative);
CU_basic_run_tests();
CU_cleanup_registry();
return 0;
}
嗯,这里要注意:CUnit 需要动态分配内存来管理测试注册表。在 Linux 或 Windows 上没问题,但放到 FreeRTOS 的堆里,可能就炸了。我在一个项目中试过把 CUnit 移植到 VxWorks,折腾了两天,最后还是换成了 Unity。
CU_initialize_registry() 会调用 malloc。如果你的嵌入式环境没有堆,或者堆很小,就别用它。我曾经在一个只有 4KB RAM 的 MCU 上试过,直接 HardFault。
4.3 Unity:嵌入式单元测试的标配
Unity 的设计哲学是“极简”。它不依赖任何标准库函数,所有断言都是宏展开。你甚至可以在中断服务函数里调用它(虽然我不建议这么干)。
看代码:
#include "unity.h"
/* 被测函数 */
int divide(int a, int b) {
if (b == 0) return -1; /* 错误处理 */
return a / b;
}
/* 测试用例 */
void setUp(void) {} /* 每个测试前执行 */
void tearDown(void) {} /* 每个测试后执行 */
void test_divide_normal(void) {
TEST_ASSERT_EQUAL_INT(2, divide(6, 3));
}
void test_divide_by_zero(void) {
TEST_ASSERT_EQUAL_INT(-1, divide(5, 0));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_divide_normal);
RUN_TEST(test_divide_by_zero);
return UNITY_END();
}
Unity 的断言宏很丰富:TEST_ASSERT_EQUAL、TEST_ASSERT_TRUE、TEST_ASSERT_BITS(位比较,嵌入式神器)。我最常用的是 TEST_ASSERT_EQUAL_HEX32,调试寄存器值的时候一目了然。
setUp 和 tearDown 是空函数,你可以自己填充。比如在 setUp 里初始化 GPIO,在 tearDown 里清理状态。这样每个测试用例都是独立的,不会互相污染。
4.4 CMock:自动生成 Mock 与 Stub
嵌入式代码最头疼的是硬件依赖。你测一个温度采集函数,它调用了 I2C_Read()。没有硬件,你怎么测?
CMock 就是干这个的。你给它一个头文件,它自动生成 Mock 函数。你可以在测试里设置“期望”:这个函数应该被调用几次?参数是什么?返回值是什么?
举个例子,假设有头文件 i2c_driver.h:
/* i2c_driver.h */
int I2C_Read(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data, uint8_t len);
用 CMock 生成 Mock 后,测试可以这样写:
#include "unity.h"
#include "mock_i2c_driver.h" /* CMock 生成的头文件 */
/* 被测函数:读取温度传感器 */
int read_temperature(void) {
uint8_t raw;
if (I2C_Read(0x48, 0x00, &raw, 1) != 0) {
return -1;
}
return (int)raw * 10; /* 假设每 LSB 代表 0.1°C */
}
void test_read_temperature_ok(void) {
uint8_t expected_raw = 0x1A; /* 26°C */
I2C_Read_ExpectAndReturn(0x48, 0x00, NULL, 1, 0);
/* 注意:CMock 会捕获 data 指针,我们需要设置返回值 */
I2C_Read_ReturnArrayThruPtr_data(&expected_raw, 1);
int temp = read_temperature();
TEST_ASSERT_EQUAL_INT(260, temp); /* 26.0°C */
}
看着有点绕?其实逻辑很简单:你告诉 CMock,“I2C_Read 会被调用,参数应该是这些,返回值应该是那个,并且通过 data 指针返回这个值”。CMock 会在测试运行时检查实际调用是否匹配。
4.5 测试桩(Stub)与模拟(Mock)技术
这两个概念经常被混用,但区别很大。我画了一张图来说明:
简单来说:
- Stub:你手写一个函数,返回固定值。比如
int GetTemperature(void) { return 25; }。它不关心被调了几次,参数是什么。适合快速隔离硬件。 - Mock:它记录调用信息,并验证是否符合预期。比如“GetTemperature 应该被调用 3 次,且每次间隔 100ms”。CMock 就是干这个的。
4.6 实战:组合使用 Unity + CMock
最后给一个完整的实战流程。假设我们要测试一个“温度报警”模块,它依赖 I2C 驱动和 GPIO 驱动。
- 准备头文件:
i2c_driver.h、gpio_driver.h - 用 CMock 生成 Mock:
ruby cmock.rb i2c_driver.h gpio_driver.h - 编写测试用例:在
test_alarm.c里包含mock_i2c_driver.h和mock_gpio_driver.h - 设置期望:比如“温度超过阈值时,GPIO 应该被拉高”
- 编译运行:用 Unity 的 runner 脚本自动生成 main 函数
代码骨架:
/* test_alarm.c */
#include "unity.h"
#include "mock_i2c_driver.h"
#include "mock_gpio_driver.h"
/* 被测函数声明 */
extern void alarm_check(void);
void test_alarm_trigger_when_high_temp(void) {
/* 模拟 I2C 读取到高温 85°C */
uint8_t temp_val = 0x55; /* 85 * 10 = 0x55 */
I2C_Read_ExpectAndReturn(0x48, 0x00, NULL, 1, 0);
I2C_Read_ReturnArrayThruPtr_data(&temp_val, 1);
/* 期望 GPIO 被拉高 */
GPIO_WritePin_Expect(ALARM_PIN, 1);
alarm_check();
}
嗯,这里要注意:CMock 的 ExpectAndReturn 系列函数,参数顺序是“期望的输入参数 + 返回值”。ReturnArrayThruPtr_data 是告诉 CMock:“当函数通过指针写数据时,把这段内容写进去”。