3、C语言单元测试框架:Unity框架介绍、Ceedling构建工具、CMock模拟框架、框架选型对比
好,咱们进入第三讲。这一讲,我打算把嵌入式C语言单元测试的“三驾马车”给讲透——Unity、Ceedling、CMock。这三个东西,说白了就是一套组合拳,专门给咱们这种搞嵌入式、搞医疗软件的工程师准备的。
我记得刚入行那会儿,团队里做单元测试,有人用CUnit,有人自己手写测试桩,那叫一个乱。后来我接触到这套工具链,第一感觉就是:嗯,这才是给嵌入式人用的东西。轻量、无依赖、跑得快。
3.1 Unity框架介绍
Unity,名字听着挺唬人,其实它就是一个纯C的单元测试框架。有多纯?就两个文件:unity.c和unity.h。你把它往工程里一扔,就能用。
核心特点:
- 极简设计:整个框架就几千行代码,没有动态内存分配,没有文件IO。说白了,就是为单片机这种资源受限的环境量身定做的。
- 断言丰富:TEST_ASSERT_EQUAL、TEST_ASSERT_TRUE、TEST_ASSERT_STRING等,覆盖了嵌入式开发中90%的测试场景。
- 可移植性:我曾在ARM Cortex-M、RISC-V、甚至8051上都跑过Unity,基本零改动。
来,看个最简单的例子:
#include "unity.h"
void setUp(void) {
// 每个测试用例执行前调用,我习惯在这里初始化变量
}
void tearDown(void) {
// 每个测试用例执行后调用,清理资源
}
void test_CalculateDose_ShouldReturnCorrectValue(void) {
int result = calculate_dose(50, 2.5);
TEST_ASSERT_EQUAL_INT(125, result);
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_CalculateDose_ShouldReturnCorrectValue);
return UNITY_END();
}
你看,结构非常清晰。每个测试用例就是一个函数,用RUN_TEST去调用。我在项目中遇到过一个问题:测试用例多了以后,main函数里RUN_TEST的列表会变得很长。后来怎么解决的?用Ceedling自动管理了。
3.2 Ceedling构建工具
Ceedling是什么?它本质上是一个构建系统,基于Ruby的Rake。但你别被Ruby吓到,你不需要会Ruby,只需要会写YAML配置文件就行。
Ceedling解决了什么问题?
- 自动发现测试用例:你只要把测试文件放在test/目录下,Ceedling会自动找到所有以test_开头的文件,并编译运行。
- 自动生成模拟(Mock):配合CMock,你只需要告诉Ceedling要模拟哪些头文件,它自动生成模拟代码。
- 持续集成友好:一条命令ceedling test:all,跑完所有测试,输出XML或JUnit格式的报告。
我给你们看一个典型的project.yml配置:
:project:
:build_root: build
:test_paths:
- test
:source_paths:
- src
:include_paths:
- include
:unity:
:vendor_path: vendor/unity
:cmock:
:mock_path: build/test/mocks
:includes:
- <stdint.h>
- <stdbool.h>
:tools:
:test_compiler:
:executable: arm-none-eabi-gcc
:arguments:
- -I"$": COLLECTION_PATHS_TEST_TOOLCHAIN_INCLUDE
- -I"$": COLLECTION_PATHS_TEST_SUPPORT_SOURCE_INCLUDE
- -D$: COLLECTION_DEFINES_TEST_AND_VENDOR
- -c "${1}"
- -o "${2}"
嗯,这里要注意::tools部分,你可以指定交叉编译器。我在做医疗设备开发时,目标芯片是STM32,就用arm-none-eabi-gcc。这样在PC上编译的测试代码,用的就是和固件一样的编译器,能发现一些跨平台问题。
3.3 CMock模拟框架
CMock,说白了就是帮你自动生成“假函数”的工具。在单元测试中,我们经常需要隔离被测模块的外部依赖,比如ADC驱动、I2C通信、定时器等等。手写这些模拟函数?太累了。CMock一键生成。
CMock的工作原理:
- 你告诉它要模拟哪个头文件(比如adc_driver.h)。
- CMock解析这个头文件,为里面的每个函数生成一个模拟版本。
- 在测试中,你可以设置期望:这个函数应该被调用几次?参数是什么?返回值是什么?
- 测试结束后,CMock会自动验证期望是否被满足。
看个实际例子。假设我们有这样一个头文件:
// adc_driver.h
#ifndef ADC_DRIVER_H
#define ADC_DRIVER_H
#include <stdint.h>
void adc_init(void);
uint16_t adc_read_channel(uint8_t channel);
#endif
在Ceedling的project.yml中配置:
:cmock:
:mock_path: build/test/mocks
:includes_h:
- adc_driver.h
然后,在测试代码中就可以这样用:
#include "unity.h"
#include "mock_adc_driver.h"
#include "temperature_sensor.h" // 被测模块
void test_ReadTemperature_ShouldConvertADCValue(void) {
// 设置期望:adc_read_channel会被调用一次,参数为0,返回1000
adc_read_channel_ExpectAndReturn(0, 1000);
int temp = read_temperature();
TEST_ASSERT_EQUAL_INT(25, temp); // 假设1000对应25度
}
你看,测试代码变得非常干净。你不需要关心adc_read_channel内部怎么实现的,你只关心它被正确调用了,并且返回了你期望的值。
3.4 框架选型对比
好,咱们来聊聊选型。市面上C语言的单元测试框架不少,除了Unity,还有CUnit、Check、Google Test(有C++绑定)等。我根据自己的经验,做个对比:
| 特性 | Unity | CUnit | Check | Google Test (C++) |
|---|---|---|---|---|
| 资源占用 | 极低(~10KB) | 中等(~50KB) | 中等(~40KB) | 较高(~200KB+) |
| 无动态内存 | 是 | 否 | 否 | 否 |
| 模拟框架集成 | CMock(原生支持) | 需手动集成 | 需手动集成 | Google Mock |
| 构建工具 | Ceedling | 无专用工具 | 无专用工具 | CMake |
| 适合场景 | 嵌入式、单片机 | 桌面应用 | 桌面应用 | 大型项目、C++ |
| IEC 62304合规性 | 高(已验证) | 中 | 中 | 中 |
为什么我强烈推荐Unity + Ceedling + CMock这套组合?说白了,三个原因:
- 轻量:能在资源受限的MCU上跑测试,甚至可以在开发板上直接运行。
- 自动化:Ceedling把编译、链接、运行、报告生成全包了,你只需要写测试用例。
- 医疗合规:Unity的代码风格和文档,非常符合IEC 62304对软件单元测试的要求。我做过几次FDA审核,审核员对Unity生成的测试报告很认可。
好,这一讲就到这里。下一讲,我会带大家手把手搭建一个完整的单元测试工程,从零开始配置Ceedling,写第一个测试用例,跑通它。到时候你们会发现,单元测试其实没那么难,关键是工具要选对。