20、单元测试框架:Unity, CMocka, Criterion 的跨平台配置与使用
单元测试这事儿,说实话,很多C语言开发者容易忽略。我早年做嵌入式的时候,也觉得“跑个main函数打印一下不就行了?”结果项目一复杂,改个全局变量,崩了三个模块,查了一整天。嗯,从那以后,我再也不敢不写单元测试了。
今天咱们聊聊三个主流的C语言单元测试框架:Unity、CMocka 和 Criterion。它们各有脾气,跨平台配置的方式也不一样。我一个个说,你跟着走一遍,基本就能上手。
核心观点:没有最好的框架,只有最适合你项目场景的框架。嵌入式裸机选Unity,Linux用户态选CMocka,追求现代语法选Criterion。
20.1 Unity:嵌入式领域的轻骑兵
Unity 是 ThrowTheSwitch 组织维护的纯C测试框架。它只有一个 .c 文件和两个 .h 文件,没有动态内存分配,没有libc依赖。你想想看,这对嵌入式开发意味着什么?意味着你可以在STM32上跑单元测试,甚至可以在没有操作系统的裸机上跑。
我在一个智能家居项目中用过Unity。当时MCU只有64KB Flash,跑不了任何重量级框架。Unity编译后只占不到2KB,简直完美。
跨平台配置步骤
- 下载源码:从GitHub拉取Unity,只需要
unity.c和unity.h两个文件。 - 目录结构:我习惯把Unity放在
test/unity/下,和测试用例分开。 - 编写测试:每个测试文件包含
setUp()和tearDown()。 - 编译运行:用你平台的编译器直接编译即可。
// test_math.c
#include "unity.h"
#include "math_utils.h"
void setUp(void) {}
void tearDown(void) {}
void test_add_positive(void) {
TEST_ASSERT_EQUAL_INT(5, add(2, 3));
}
void test_add_negative(void) {
TEST_ASSERT_EQUAL_INT(-1, add(-3, 2));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_add_positive);
RUN_TEST(test_add_negative);
return UNITY_END();
}
我的小技巧:在嵌入式平台上,我会把 main() 改成通过串口输出测试结果。这样不用接调试器,插上USB看串口助手就能知道测试是否通过。
跨平台编译命令
| 平台 | 编译命令 |
|---|---|
| Linux/macOS | gcc test_math.c unity.c -o test_math -I. -Iunity/ |
| Windows (MinGW) | gcc test_math.c unity.c -o test_math.exe -I. -Iunity/ |
| ARM GCC (嵌入式) | arm-none-eabi-gcc test_math.c unity.c -o test_math.elf -I. -Iunity/ --specs=nosys.specs |
注意:嵌入式平台编译时,如果链接器报 _exit 未定义,加上 --specs=nosys.specs 或者自己实现一个空 _exit()。我曾经在这上面卡了半小时,后来发现Unity的 UNITY_END() 会调用 exit()。
20.2 CMocka:服务端测试的瑞士军刀
CMocka 是 Google 出品的C测试框架,继承了 Cmockery 的血统。它最大的亮点是内置了Mock功能——说白了,就是你可以假装调用了一个函数,然后让它返回你指定的值。这在测试网络、文件IO、数据库等外部依赖时特别有用。
我记得有一次测试一个HTTP客户端库,网络请求不能每次都真的发出去。用CMocka的 will_return() 和 mock(),我把 send() 和 recv() 全部Mock掉,测试跑得又快又稳。
跨平台配置步骤
- 安装依赖:CMocka 需要 cmake 构建。Linux上
apt install libcmocka-dev,macOS上brew install cmocka。 - Windows编译:从源码用cmake生成VS工程,或者用vcpkg安装。
- 编写测试:使用
cmocka.h提供的宏。 - 链接库:需要链接
-lcmocka。
// test_http.c
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include "http_client.h"
/* Mock send函数 */
ssize_t __wrap_send(int sockfd, const void *buf, size_t len, int flags) {
check_expected_ptr(buf);
return mock_type(ssize_t);
}
void test_http_get(void **state) {
will_return(__wrap_send, 1024);
expect_string(__wrap_send, buf, "GET /api/data HTTP/1.1");
int ret = http_get("http://example.com/api/data");
assert_int_equal(ret, 200);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_http_get),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
关键点:CMocka的Mock是通过链接器的wrap机制实现的。编译时加上 -Wl,--wrap=send,链接器会把对 send 的调用替换成 __wrap_send。这个技巧在Linux上很成熟,但Windows上需要额外配置。
跨平台编译命令
| 平台 | 编译命令 |
|---|---|
| Linux | gcc test_http.c -o test_http -lcmocka -Wl,--wrap=send |
| macOS | gcc test_http.c -o test_http -lcmocka -Wl,-wrap,send |
| Windows (MSVC) | 需要修改链接器配置,或者使用Detours库替代wrap |
避坑指南:我曾经在Windows上用CMocka的wrap功能,结果MSVC不支持这个GCC特性。后来我改用 --wrap 配合MinGW才搞定。如果你必须用MSVC,建议用Criterion或者自己写函数指针替换。
20.3 Criterion:现代C测试的优雅之选
Criterion 是这三个里面最“现代”的。它支持参数化测试、超时控制、信号处理,甚至还能自动检测浮点异常。语法上有点像Catch2,写起来很舒服。
我个人特别喜欢它的参数化测试——你想想看,以前测试一个函数要写10个 RUN_TEST,现在一行 ParamTest 就搞定了。我在一个数据处理项目中,用Criterion的参数化测试覆盖了200多组输入输出,代码量反而比之前少了。
跨平台配置步骤
- 安装:Linux上
apt install libcriterion-dev,macOS上brew install criterion。 - Windows:用vcpkg安装
vcpkg install criterion,或者从源码用cmake构建。 - 编写测试:使用
criterion/criterion.h。 - 链接:需要
-lcriterion。
// test_sort.c
#include <criterion/criterion.h>
#include "sort.h"
/* 普通测试 */
Test(sort, bubble_sort_basic) {
int arr[] = {3, 1, 4, 1, 5};
bubble_sort(arr, 5);
cr_expect_arr_eq(arr, (int[]){1, 1, 3, 4, 5}, 5);
}
/* 参数化测试 */
ParamTest(sort, bubble_sort_params, int *arr, int *expected, int len) {
bubble_sort(arr, len);
cr_expect_arr_eq(arr, expected, len);
}
ParamTest(sort, bubble_sort_params,
.params = {
{ (int[]){3, 1, 2}, (int[]){1, 2, 3}, 3 },
{ (int[]){5, 4, 3, 2, 1}, (int[]){1, 2, 3, 4, 5}, 5 },
{ (int[]){1}, (int[]){1}, 1 },
}
);
/* 超时测试 */
Test(sort, infinite_loop, .timeout = 1) {
int arr[] = {2, 1};
bubble_sort(arr, 2);
cr_expect_eq(arr[0], 1);
}
我的习惯:在CI环境中,我会给每个测试加上 .timeout 参数。有一次一个死循环bug在本地没复现,CI上因为超时机制直接报错,帮我省了一下午的调试时间。
跨平台编译命令
| 平台 | 编译命令 |
|---|---|
| Linux | gcc test_sort.c -o test_sort -lcriterion |
| macOS | gcc test_sort.c -o test_sort -lcriterion |
| Windows (MSVC) | cl test_sort.c /Ipath\to\criterion /link criterion.lib |
20.4 跨平台配置的通用建议
说了三个框架的具体配置,我再聊聊通用的跨平台策略。这些是我在多个项目中踩坑总结出来的。
- 使用CMake统一构建:不要手写Makefile。CMake可以自动检测平台和编译器,选择正确的链接库。Unity、CMocka、Criterion都有官方的CMake支持。
- 条件编译处理平台差异:比如Windows上没有
fork(),CMocka的某些功能会受限。用#ifdef _WIN32做适配。 - CI中多平台测试:GitHub Actions或者GitLab CI上,配置Linux、macOS、Windows三个runner,每个平台都跑一遍测试。我见过太多“Linux上好好的,Windows上崩了”的案例。
- 测试代码也要做静态分析:用cppcheck或者clang-tidy检查测试代码。测试代码也是代码,写错了照样会误导你。
总结一下:Unity适合资源受限的嵌入式环境,CMocka适合需要Mock外部依赖的服务端项目,Criterion适合追求开发效率和现代语法的团队。没有银弹,选你用得最顺手的那个。
嗯,单元测试框架的配置和使用,说到底就是“选一个框架,配好编译环境,然后坚持写测试”。我见过太多项目一开始信誓旦旦要写单元测试,结果两周后就放弃了。别这样,坚持下来,你会发现代码质量真的不一样。