第4章:自动化测试框架选型——嵌入式测试框架对比与选择策略

说实话,选测试框架这事儿,我当年也纠结过好一阵子。

那时候刚接手一个嵌入式项目,代码量不大,但硬件资源紧张得要命。ROM只剩几十KB,RAM更是抠抠搜搜。我一开始想用Google Test,结果编译完发现,光框架本身就把ROM吃掉了一半。嗯,那会儿我就明白了——嵌入式测试框架选型,跟做菜选锅一样,得看灶台有多大。

4.1 主流嵌入式测试框架对比

我这些年用过的框架不少,挑四个最有代表性的说说:CUnit、Unity、CMock、Google Test。它们各有各的脾气。

框架 语言 资源占用 适用场景 我的评价
CUnit C 中等 PC端测试、资源较丰富的嵌入式 老牌,但有点笨重
Unity C 极低 MCU、资源受限的嵌入式 轻量,我的首选
CMock C 低(运行时) 需要mock硬件依赖的场景 配合Unity用,绝配
Google Test C++ Linux、ARM Linux、资源充足的系统 功能强,但别用在MCU上

4.2 各框架深度解析

CUnit:老当益壮,但有点胖

CUnit是最早一批C语言测试框架。它的设计思路跟JUnit很像,有测试套件、测试用例、断言这些概念。我在一个PC端的嵌入式模拟器项目里用过它,功能确实全。

但问题来了——它依赖动态内存分配。你想想看,在裸机MCU上,malloc/free可不是随便能用的。我有个同事硬要在STM32上跑CUnit,结果堆区爆了,调试了一整天。

注意:CUnit在资源小于64KB ROM的MCU上,基本跑不起来。别问我怎么知道的。

Unity:我的心头好

Unity是我个人最推荐的嵌入式测试框架。它有多轻?整个框架就两个文件:unity.c和unity.h。我算过,编译后ROM占用不到4KB。

它的用法也简单到令人发指:

#include "unity.h"

void setUp(void) {}
void tearDown(void) {}

void test_led_on(void) {
    TEST_ASSERT_TRUE(led_is_on());
}

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_led_on);
    return UNITY_END();
}

你看,没有花里胡哨的东西。我在一个Cortex-M0的项目里,ROM总共32KB,Unity跑得稳稳当当。说白了,它就是为嵌入式而生的。

CMock:搞定硬件依赖

嵌入式测试最头疼的是什么?硬件依赖。你总不能每次测试都真的去点灯、读传感器吧?CMock就是干这个的。

它可以根据头文件自动生成mock函数。比如你有个i2c_read()函数,CMock能生成一个假的版本,让你控制它返回什么值。

技巧:我习惯把CMock和Unity搭配使用。Unity负责测试框架,CMock负责模拟硬件。这两个都是ThrowTheSwitch组织的项目,兼容性极好。

Google Test:功能强大,但别乱用

Google Test(简称GTest)功能确实强。参数化测试、死亡测试、mock框架(Google Mock)……应有尽有。但代价也大。

我测过一个项目,GTest编译后ROM占用超过200KB。这在ARM Linux上没问题,但在STM32上?想都别想。我曾经见过有人硬要在FreeRTOS上跑GTest,结果任务栈直接溢出。

我的建议:如果你的目标平台是Linux、或者有足够RAM(>512KB)和ROM(>1MB)的嵌入式系统,GTest是首选。否则,老老实实用Unity。

4.3 框架选择策略

选框架这事儿,我总结了一个三步法:

  1. 看资源:先看目标平台的ROM和RAM。小于64KB ROM的,Unity是唯一选择。大于512KB的,可以考虑GTest。
  2. 看依赖:如果被测代码跟硬件耦合很深,必须用CMock或类似mock工具。Unity+CMock是黄金组合。
  3. 看团队:团队熟悉C++吗?如果全员C语言老手,别硬上GTest。我见过一个团队为了用GTest,把整个项目改成C++编译,结果兼容性问题一堆。

另外,我建议你做一个快速原型验证。花一天时间,用Unity写几个测试用例,编译到目标板上跑跑。如果跑得通,就继续用。跑不通,再换。别在选型阶段纠结太久。

4.4 我在项目中踩过的坑

坑踩多了,自然就记住了。分享几个典型的:

  • 坑一:Unity的TEST_ASSERT_EQUAL是整型比较。我曾经用它比较两个浮点数,结果一直失败。后来才发现,浮点数比较要用TEST_ASSERT_FLOAT_WITHIN。嗯,文档还是要看的。
  • 坑二:CMock生成的mock函数默认不检查参数。我有个同事写了个测试,mock了i2c_write(),但没检查传入的数据对不对。结果测试全绿,实际代码里传的参数全是错的。后来我强制团队在mock里加参数检查。
  • 坑三:GTest的SetUp/TearDown在嵌入式上可能不执行。我记得有一次在ARM Linux上跑GTest,SetUp函数莫名其妙没调用。查了半天,发现是编译器优化把测试固件给优化掉了。解决方案是加volatile关键字。
  • 坑四:别在中断里跑测试。这个我犯过。有一次在中断服务函数里调用了Unity的断言,结果系统直接死机。因为Unity的断言会调用printf,而printf不是中断安全的。
血的教训:测试框架本身也是代码,也会引入bug。我遇到过Unity的一个旧版本在ARM Cortex-M4上有断言失败的问题。所以,请使用最新稳定版。

4.5 我的最终推荐

如果你问我,现在新开一个嵌入式项目,用什么测试框架?

我的答案是:Unity + CMock。轻量、稳定、社区活跃。除非你的平台是Linux或者资源极其充裕,否则别碰GTest。

当然,这只是我的个人习惯。你完全可以根据自己的项目情况调整。但记住一点:测试框架是工具,不是目的。别为了用某个框架而强行改造你的项目。

好了,这一章就聊到这儿。下一章我们讲讲如何搭建自动化测试环境,包括如何在Jenkins里跑Unity测试。到时候我会分享一些CI/CD的配置技巧,保证实用。