3. 单元测试核心概念
做嵌入式单元测试,选对工具能省一半力气。市面上框架不少,但真正在嵌入式圈子里站稳脚跟的,还得是Unity、Ceedling和CMock这三兄弟。
我最早接触的是Unity。那时候项目紧,老板说“你给我加个测试”,我翻遍了资料,最后选了它。为什么?因为它就两个文件,往工程里一丢就能跑。你想想看,嵌入式环境多复杂,动不动就是交叉编译、资源受限,Unity这种轻量级设计简直就是为我们量身定做的。
3.1 Unity:嵌入式测试的“瑞士军刀”
Unity是一个纯C语言的单元测试框架。它的核心思想很简单:用宏来定义测试用例,用断言来检查结果。
先看个例子:
#include "unity.h"
void setUp(void) {}
void tearDown(void) {}
void test_加法_正数相加(void) {
TEST_ASSERT_EQUAL_INT32(5, add(2, 3));
}
void test_加法_负数相加(void) {
TEST_ASSERT_EQUAL_INT32(-5, add(-2, -3));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_加法_正数相加);
RUN_TEST(test_加法_负数相加);
return UNITY_END();
}
看到没?TEST_ASSERT_EQUAL_INT32 就是核心断言。Unity提供了几十种断言宏,覆盖了整数、浮点、字符串、内存块等各种比较场景。
Unity的核心特点:
- 纯C实现,无任何外部依赖
- ROM/RAM占用极小(约2-4KB)
- 支持跨平台编译(ARM、AVR、RISC-V等)
- 断言失败自动跳转到下一个测试,不会卡死
我记得有一次在STM32上跑测试,板子Flash只剩8KB了。其他框架根本塞不进去,Unity却轻松搞定。这就是它的价值所在。
3.2 CMock:自动生成Mock对象的利器
做单元测试最头疼的是什么?是依赖。你的函数调用了ADC、调用了I2C、调用了Flash驱动……这些硬件接口在PC上根本跑不了。
CMock就是来解决这个问题的。它能根据头文件自动生成Mock函数,让你在不依赖真实硬件的情况下完成测试。
用法很简单:
// 假设你有这样一个头文件
// adc_driver.h
int ADC_Read(uint8_t channel);
void ADC_Init(void);
// CMock会根据它自动生成mock
// 在测试中你可以这样用
void test_温度采集_正常(void) {
ADC_Read_ExpectAndReturn(1, 25);
int temp = read_temperature(1);
TEST_ASSERT_EQUAL_INT32(25, temp);
}
这里的关键是 ADC_Read_ExpectAndReturn。它告诉CMock:“我期望ADC_Read被调用,参数是1,返回值是25”。如果实际调用时参数不对,或者根本没调用,测试就会失败。
我的经验:CMock最强大的地方在于它能验证调用顺序。你可以用 ExpectAndReturn 链式调用,确保函数按你期望的顺序执行。这在测试状态机时特别有用。
3.3 Ceedling:把一切串起来的构建工具
Unity和CMock都是库,但怎么组织测试工程?怎么自动发现测试用例?怎么生成测试报告?
Ceedling就是干这个的。它是一个基于Ruby的构建系统,专门为嵌入式C项目设计。你只需要一个 project.yml 配置文件,它就能帮你搞定一切。
# project.yml 示例
:project:
:build_root: build
:test_paths:
- test
:source_paths:
- src
:unity:
:path: vendor/unity
:cmock:
:mock_paths:
- test/mocks
:includes:
- "adc_driver.h"
- "i2c_driver.h"
配置好之后,一行命令就能跑所有测试:
ceedling test:all
Ceedling会自动扫描 test/ 目录下的所有测试文件,编译、链接、运行,最后生成漂亮的HTML报告。
3.4 三者的关系与选型对比
说白了,这三者的关系是这样的:
- Unity:测试框架本体,负责断言和测试执行
- CMock:Mock生成器,解决依赖问题
- Ceedling:构建工具,把前两者整合起来
你可以只用Unity,手动写Mock。但项目大了之后,Ceedling+CMock的组合能省下大量时间。
3.5 选型对比表
我把三者的关键差异整理成了表格,方便你快速对比:
| 特性 | Unity | CMock | Ceedling |
|---|---|---|---|
| 定位 | 测试框架 | Mock生成器 | 构建管理工具 |
| 语言 | C | C + Ruby | Ruby |
| 依赖 | 无 | Unity | Unity + CMock + Ruby |
| ROM占用 | ~2-4KB | 取决于Mock数量 | 不占用目标ROM |
| 学习曲线 | 低 | 中 | 中高 |
| 适用场景 | 资源受限、小项目 | 依赖多、中等项目 | 大型项目、团队协作 |
3.6 避坑指南
我踩过不少坑,分享几个给你:
我曾经犯过的错:
- CMock头文件路径问题:CMock需要解析头文件来生成Mock。如果头文件里有
#include其他文件,路径必须配对。我一开始没注意,结果生成了一堆空Mock,排查了半天。 - Ceedling的Ruby版本:Ceedling依赖Ruby 2.5以上。有次我在老服务器上部署,Ruby版本太低,折腾了一天才发现是版本问题。
- Unity的setUp/tearDown:每个测试用例都会调用setUp和tearDown。如果你在setUp里分配了动态内存,别忘了在tearDown里释放。不然跑几百个用例后,内存就爆了。
我的建议:
- 新手从Unity开始,先跑通一个用例再说
- 遇到硬件依赖时,再引入CMock
- 项目超过10个源文件,果断上Ceedling
- 别在CMock的配置上省时间,
project.yml写清楚能省很多麻烦
3.7 小结
Unity、CMock、Ceedling这三者,说白了就是一套从“能跑”到“好管”的进化路径。Unity解决的是“能不能测”的问题,CMock解决的是“依赖怎么处理”的问题,Ceedling解决的是“测试工程怎么组织”的问题。
我个人习惯是:小项目只用Unity,手动写几个Mock就够了。中等项目上Unity+CMock,自动生成Mock省时间。大项目或者团队协作,直接上Ceedling全家桶,自动化构建、测试报告、持续集成一条龙。
嗯,选型没有银弹。关键是根据你的项目规模、资源限制和团队能力来定。别一上来就上全套,也别一直停留在手动阶段。找到那个平衡点,才是真本事。