单元测试框架:CUnit、Unity、CMock入门与实战
说实话,我见过太多C语言项目,代码写得漂亮,但一跑起来就崩。为什么?因为没做单元测试。很多人觉得嵌入式或者底层C代码没法测,其实这是个误区。今天我就带你看看三个主流的C单元测试框架——CUnit、Unity、CMock,它们怎么用,以及我在实际项目中踩过的坑。
为什么C语言需要单元测试?
你想想看,一个函数几十行,你凭什么相信它是对的?凭肉眼review?我年轻时也这么干,结果被bug追着跑。单元测试说白了,就是给每个函数写个小剧本,验证它是不是按剧本演。
C语言的单元测试有个特点:它不像Java有JUnit那么成熟,但胜在轻量。你不需要装什么IDE插件,一个测试文件、一个框架头文件,就能跑起来。嗯,这就是C的风格——简单粗暴但有效。
核心价值:单元测试不是为了证明代码没错,而是为了让你改代码时心里有底。我每次重构前,第一件事就是补测试。
CUnit:老牌框架,功能全面
CUnit是我最早接触的框架。它像个瑞士军刀,啥都有——测试注册、套件管理、多种输出格式。但说实话,API有点啰嗦。
看个例子:
#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("math_tests", 0, 0);
CU_add_test(suite, "test_add_positive", test_add_positive);
CU_add_test(suite, "test_add_negative", test_add_negative);
CU_basic_run_tests();
CU_cleanup_registry();
return 0;
}
看到没?你得先初始化注册表,再建套件,再加测试。我在项目中用过一段时间,后来发现一个问题——测试文件写多了,光这些模板代码就占了一半篇幅。不过它的断言种类很全,CU_ASSERT_EQUAL、CU_ASSERT_PTR_NULL这些都有,适合做精细控制。
我的习惯:如果项目已经用了CUnit,别急着换。它稳定,文档全,社区也大。但新项目我建议看看Unity。
Unity:轻量级,嵌入式首选
Unity是我现在的主力框架。它只有一个.c和一个.h文件,加起来不到2000行。你想想看,这多适合嵌入式环境?我有个项目跑在STM32上,RAM只有64K,Unity照样跑得欢。
它的用法更简洁:
#include "unity.h"
void setUp(void) {}
void tearDown(void) {}
void test_add_should_return_5(void) {
TEST_ASSERT_EQUAL_INT(5, add(2, 3));
}
void test_add_should_handle_negative(void) {
TEST_ASSERT_EQUAL_INT(-3, add(-1, -2));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_add_should_return_5);
RUN_TEST(test_add_should_handle_negative);
return UNITY_END();
}
你看,没有套件概念,直接RUN_TEST就行。setUp和tearDown是钩子函数,每个测试前后自动调用。我一般把malloc和free放里面,防止内存泄漏。
为什么会喜欢Unity?因为它够简单。你不需要学一堆API,看半小时文档就能上手。而且它的输出很友好,失败时会把期望值和实际值都打印出来。
注意:Unity的断言宏是TEST_ASSERT_XXX,不是CU_ASSERT。别搞混了。我曾经把CUnit的宏直接拷过来,编译报错找了半天。
CMock:模拟依赖,隔离测试
单元测试最难的是什么?是处理依赖。比如你的函数调用了硬件API、文件系统、或者网络库。这些在测试环境里跑不了。怎么办?用Mock。
CMock是Unity的搭档,它能自动生成模拟函数。你给它一个头文件,它就能生成对应的mock代码。比如你有个传感器驱动:
// sensor.h
int sensor_read_temperature(void);
int sensor_read_humidity(void);
用CMock生成mock后,你就可以在测试里控制返回值:
#include "unity.h"
#include "mock_sensor.h"
void test_process_sensor_data(void) {
sensor_read_temperature_ExpectAndReturn(25);
sensor_read_humidity_ExpectAndReturn(60);
int result = process_sensor_data();
TEST_ASSERT_EQUAL_INT(1, result);
}
这里的关键是ExpectAndReturn——它告诉mock:我期望这个函数被调用,并且返回指定值。如果函数没被调用,或者调用次数不对,测试会失败。
我曾经在一个物联网项目里用CMock模拟了WiFi模块。当时硬件还没到,但软件已经测完了。你想想看,这省了多少时间?
实战建议:CMock生成的代码有点大,嵌入式环境要注意ROM空间。我一般只在PC上跑mock测试,板子上跑集成测试。
知识体系总览
下面这张图是我自己整理的,帮你理清这三个框架的关系和适用场景:
实战:如何选择与组合
我个人的经验是这样的:
- 如果你在PC上开发,项目规模大:用CUnit。它的测试套件管理能力强,适合多人协作。
- 如果你做嵌入式,资源受限:用Unity。它小到可以塞进任何平台。
- 如果你需要模拟外部依赖:用CMock。它和Unity是天生一对。
我现在的标准配置是Unity + CMock。Unity负责测试框架,CMock负责模拟。两个加起来不到3000行代码,但能覆盖90%的测试场景。
一个小技巧:用CMock时,记得在头文件里用#ifndef保护。不然多次包含会导致mock生成重复代码。我曾经因为这个编译错误搞了一下午。
避坑指南
最后分享几个我踩过的坑:
- 测试不要依赖执行顺序:每个测试应该是独立的。我见过有人测试A里设了个全局变量,测试B依赖它。结果换个编译器顺序变了,测试全挂。
- 不要测试私有函数:只测公开接口。私有函数是内部实现,改了测试就得跟着改,维护成本太高。
- Mock不要过度:只mock外部依赖,不要mock自己模块的内部函数。否则测试就变成了"测试mock是否正确",失去了意义。
嗯,单元测试这东西,刚开始觉得麻烦,但用习惯了就离不开了。我现在写任何C代码,第一件事就是搭好测试框架。你试试看,一个月后你会回来感谢我的。
公众号:蓝海资料掘金营,微信deep3321