3. 嵌入式C语言单元测试框架:Unity、Ceedling、CMock简介与选型对比

做嵌入式单元测试,选对工具能省一半力气。市面上框架不少,但真正在嵌入式圈子里站稳脚跟的,还得是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的组合能省下大量时间。

Unity / CMock / Ceedling 关系与选型决策图 Unity 测试框架 / 断言宏 CMock Mock对象生成器 Ceedling 构建 / 测试管理 选型决策:根据项目规模与资源选择组合 方案A:轻量级 仅 Unity 手动写 Mock 适合小项目 方案B:标准级 Unity + CMock 自动生成 Mock 适合中等项目 方案C:全功能 Unity + CMock + Ceedling 自动化构建与报告 适合大型项目 💡 选型建议:资源受限选A,中等复杂度选B,团队协作选C Ceedling 需要 Ruby 环境,部分嵌入式开发环境可能不支持

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全家桶,自动化构建、测试报告、持续集成一条龙。

嗯,选型没有银弹。关键是根据你的项目规模、资源限制和团队能力来定。别一上来就上全套,也别一直停留在手动阶段。找到那个平衡点,才是真本事。