3、单元测试框架选型:CUnit、Unity、CMock、Google Test 的对比与选择

做嵌入式中间件测试,第一步就是选框架。

说实话,这步选错了,后面全是坑。我见过不少团队,框架选得花里胡哨,结果跑在单片机上直接崩了。嗯,今天咱们就把四个主流框架掰开揉碎聊一聊。

3.1 先看一张全景图

在深入细节之前,我习惯先画一张图,把四个框架的定位搞清楚。你想想看,它们其实分属两个不同的赛道:

嵌入式单元测试框架选型全景图 纯 C 语言测试框架 C++ 测试框架 CUnit 老牌框架,功能全面但体积大 Unity 轻量级,专为嵌入式设计 CMock Mock 生成工具,配合 Unity 使用 Google Test 功能最强大,但依赖 C++ 环境 资源受限 功能丰富 选型核心:看你的目标平台资源 + 团队技术栈

这张图我每次培训都会放。说白了,选型就两个维度:资源消耗功能丰富度。你不可能在 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_INTTEST_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 我的选型建议

做了这么多年嵌入式测试,我总结了一套选型原则:

  1. 裸机或 RTOS 项目:无脑选 Unity + CMock。轻量、高效、够用。
  2. Linux 或 Android 平台:选 Google Test。功能全面,社区支持好。
  3. 遗留项目维护:如果已经在用 CUnit,可以继续用。但新项目不建议入坑。
  4. 混合场景:PC 端用 Google Test 做集成测试,目标板用 Unity 做单元测试。

一个小技巧: 我习惯在项目初期就搭好两套测试环境。一套在 PC 上跑(用 Google Test),一套交叉编译到目标板(用 Unity)。这样既能快速验证逻辑,又能保证最终在硬件上跑得通。

最后说一句:框架只是工具,测试思维才是核心。别在选型上纠结太久,先跑起来再说。


公众号:蓝海资料掘金营,微信deep3321