3、单元测试框架选型:CUnit、Unity、CMock、Google Test 的对比与选择
做嵌入式中间件测试,第一步就是选框架。
说实话,这步选错了,后面全是坑。我见过不少团队,框架选得花里胡哨,结果跑在单片机上直接崩了。嗯,今天咱们就把四个主流框架掰开揉碎聊一聊。
3.1 先看一张全景图
在深入细节之前,我习惯先画一张图,把四个框架的定位搞清楚。你想想看,它们其实分属两个不同的赛道:
这张图我每次培训都会放。说白了,选型就两个维度:资源消耗和功能丰富度。你不可能在 Cortex-M0 上跑 Google Test,对吧?
3.2 CUnit:老大哥,但有点笨重
CUnit 是最早的 C 语言单元测试框架之一。我刚开始做嵌入式测试时,用的就是它。
优点:
- API 设计成熟,文档齐全
- 支持多种输出格式(XML、文本)
- 有图形界面版本(虽然嵌入式用不上)
缺点:
- 体积大,静态链接后轻松上百 KB
- 依赖标准库,在裸机环境下移植困难
- 测试用例注册方式繁琐
典型代码示例:
#include "CUnit/CUnit.h"
#include "CUnit/Basic.h"
void test_ringbuf_push(void) {
ringbuf_t buf;
ringbuf_init(&buf, 10);
CU_ASSERT_EQUAL(ringbuf_push(&buf, 0x55), 0);
}
int main() {
CU_initialize_registry();
CU_pSuite suite = CU_add_suite("ringbuf_suite", NULL, NULL);
CU_add_test(suite, "test_push", test_ringbuf_push);
CU_basic_run_tests();
CU_cleanup_registry();
return 0;
}
你看,光注册测试用例就要写好几行。我个人觉得,在资源敏感的嵌入式项目里,CUnit 已经不太合适了。
3.3 Unity:嵌入式测试的瑞士军刀
Unity 是我目前最常用的框架。为什么?因为它就是为嵌入式而生的。
核心特点:
- 全部用 C 语言宏实现,没有动态内存分配
- 整个框架就两个文件:unity.c 和 unity.h
- 编译后体积可以控制在 10KB 以内
我的经验: 在 STM32F103 上跑 Unity,RAM 占用不到 2KB。这在资源受限的 MCU 上太重要了。
代码示例:
#include "unity.h"
#include "ringbuf.h"
static ringbuf_t buf;
void setUp(void) {
ringbuf_init(&buf, 16);
}
void tearDown(void) {
// 清理工作
}
void test_push_should_succeed(void) {
TEST_ASSERT_EQUAL_INT(0, ringbuf_push(&buf, 0xAA));
}
void test_pop_should_return_pushed_data(void) {
ringbuf_push(&buf, 0xBB);
uint8_t data;
ringbuf_pop(&buf, &data);
TEST_ASSERT_EQUAL_HEX8(0xBB, data);
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_push_should_succeed);
RUN_TEST(test_pop_should_return_pushed_data);
return UNITY_END();
}
嗯,这里要注意:Unity 的断言宏命名很直观,TEST_ASSERT_EQUAL_INT、TEST_ASSERT_EQUAL_HEX8,一看就懂。
3.4 CMock:Mock 利器,但别滥用
CMock 是 Unity 的黄金搭档。它可以根据头文件自动生成 Mock 函数。
我曾经在一个项目中,需要测试一个依赖硬件抽象层的模块。手动写 Mock 函数写到崩溃。后来换成 CMock,一行命令搞定。
使用方式:
# 根据 hal.h 生成 Mock 函数
ruby cmock.rb -o hal.h
# 在测试中使用
#include "mock_hal.h"
void test_sensor_read(void) {
// 设置期望值
hal_adc_read_ExpectAndReturn(0, 1024);
// 调用被测函数
uint16_t val = sensor_read_temperature();
// 验证结果
TEST_ASSERT_EQUAL_UINT16(250, val);
}
避坑指南: 我曾经见过一个团队,所有函数都做 Mock,结果测试代码比业务代码还多。记住:只 Mock 外部依赖,不要 Mock 内部函数。
3.5 Google Test:功能最强,但门槛最高
Google Test 是 C++ 框架,功能确实强大。但说实话,在嵌入式领域,它的应用场景有限。
什么时候用?
- 你的目标平台跑 Linux 或 Android
- 中间件代码可以在 PC 上交叉编译测试
- 团队熟悉 C++ 和面向对象编程
代码示例:
#include
#include "ringbuf.hpp"
class RingBufTest : public ::testing::Test {
protected:
void SetUp() override {
buf = new RingBuf(16);
}
void TearDown() override {
delete buf;
}
RingBuf* buf;
};
TEST_F(RingBufTest, PushAndPop) {
buf->push(0x55);
EXPECT_EQ(buf->pop(), 0x55);
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
你看,Google Test 的测试夹具(Test Fixture)机制确实优雅。但代价是:需要 C++ 编译器、标准库支持,还有异常处理。这些在裸机环境下都是奢侈品。
3.6 选型对比表
| 维度 | CUnit | Unity | CMock | Google Test |
|---|---|---|---|---|
| 语言 | C | C | C(生成器用 Ruby) | C++ |
| 代码体积 | 大(>100KB) | 小(<10KB) | 中等(取决于 Mock 数量) | 大(>500KB) |
| RAM 占用 | 高 | 低 | 中等 | 高 |
| Mock 支持 | 无 | 无(需配合 CMock) | 自动生成 | 内置(GMock) |
| 裸机支持 | 困难 | 优秀 | 良好 | 不支持 |
| 学习曲线 | 中等 | 低 | 中等 | 高 |
| 社区活跃度 | 低 | 高 | 中等 | 非常高 |
3.7 我的选型建议
做了这么多年嵌入式测试,我总结了一套选型原则:
- 裸机或 RTOS 项目:无脑选 Unity + CMock。轻量、高效、够用。
- Linux 或 Android 平台:选 Google Test。功能全面,社区支持好。
- 遗留项目维护:如果已经在用 CUnit,可以继续用。但新项目不建议入坑。
- 混合场景:PC 端用 Google Test 做集成测试,目标板用 Unity 做单元测试。
一个小技巧: 我习惯在项目初期就搭好两套测试环境。一套在 PC 上跑(用 Google Test),一套交叉编译到目标板(用 Unity)。这样既能快速验证逻辑,又能保证最终在硬件上跑得通。
最后说一句:框架只是工具,测试思维才是核心。别在选型上纠结太久,先跑起来再说。
公众号:蓝海资料掘金营,微信deep3321