4. 单元测试框架选型与定制

航空嵌入式C/C++代码的单元测试与通用软件有本质差异:目标环境通常禁止动态内存分配,编译器与链接脚本受DO-178C约束,测试桩必须可追溯、可审计。因此框架选型不能只看“是否流行”,而要看它能否在无堆、无异常、无操作系统的约束下完成断言、打桩与覆盖率采集。本节围绕三条主线展开:主流框架横向比较、静态内存分配支持、断言与桩函数机制。

一、主流框架横向比较

航空嵌入式单元测试常用框架可分为三类。第一类是Unity + CMock + Ceedling组合,纯C实现,全部数据结构静态声明,通过宏在编译期生成测试注册表,天然适配MISRA C与无堆环境。第二类是GoogleTest / GoogleMock,C++模板实现,断言丰富、匹配器强大,但内部大量使用std::string、std::vector与异常,在裸机或受限RTOS上需要裁剪甚至不可用,通常只用于宿主机(host)侧测试。第三类是CppUTest / CppUTestExt,C++实现但提供内存泄漏检测与静态分配插件,介于两者之间。

选型时建议按以下维度打分:语言支持(纯C / C++)、是否依赖堆、断言粒度、桩/模拟能力、覆盖率集成、认证证据可追溯性。以某飞控软件为例,其单元测试运行在PowerPC目标板,堆区仅保留给启动阶段,测试代码必须零malloc。此时GoogleTest直接出局,Unity+CMock成为首选;若团队已有大量C++类需要测试,则可在宿主机用GoogleTest做快速回归,目标板用Unity做认证测试,形成“双轨”策略。

二、静态内存分配支持

静态内存分配的核心是:所有测试夹具、桩记录、断言缓冲区在编译期确定大小,运行期只做赋值与比较。Unity通过UNITY_FIXTURE_...宏与UnityFixture结构体实现,测试用例的setUp/tearDown不申请内存。CMock生成的mock对象默认使用静态数组保存调用记录,例如:

// CMock生成的mock结构(简化)
typedef struct {
    int call_count;
    struct {
        uint8_t channel;
        uint16_t value;
    } calls[CMOCK_MOCK_MAX_CALLS]; // 编译期固定
} Mock_AdcDriver;

若被测函数可能被调用上千次,需在cmock_config.yml中调整:max_calls,否则记录溢出会触发断言失败。CppUTest则通过MemoryLeakDetector与TestMemoryAllocator替换全局new/delete,在测试开始前预分配固定内存池。实操中应检查链接后的.bss与.data段大小,确保测试镜像不超过目标板RAM预算。例如某项目限制测试镜像≤64KB,通过将CMOCK_MOCK_MAX_CALLS从默认100降到20,节省约12KB。

三、断言与桩函数机制

断言是测试的“判决器”。Unity提供TEST_ASSERT_EQUAL_INT、TEST_ASSERT_FLOAT_WITHIN等宏,失败时记录文件、行号与表达式,并可通过Unity.TestFailures统计。对浮点比较,航空代码常用TEST_ASSERT_FLOAT_WITHIN(0.001f, expected, actual),避免直接相等带来的精度陷阱。GoogleTest的EXPECT_NEAR语义类似,但依赖异常传播,在-fno-exceptions下需改用ASSERT_*并关闭异常。

桩函数(stub)用于替换被测代码依赖的外部模块,如传感器驱动、总线接口。CMock根据头文件自动生成mock,支持_ExpectAndReturn、_StubWithCallback、_IgnoreAndReturn三种模式。以ADC读取为例:

// 测试用例:验证温度采集逻辑
void test_ReadTemperature_ShouldReturn25C(void) {
    AdcDriver_Read_ExpectAndReturn(ADC_CH_TEMP, 0x0A00); // 期望调用
    AdcDriver_Read_IgnoreArg_channel();                  // 忽略通道参数
    float temp = ReadTemperature();
    TEST_ASSERT_FLOAT_WITHIN(0.5f, 25.0f, temp);
}

若被测函数内部有分支依赖返回值,可用_StubWithCallback注入自定义函数,实现“按调用次数返回不同值”的序列桩。对于无法自动生成的复杂接口(如带函数指针的结构体),可手写桩文件,但必须纳入配置管理并标注“测试专用”。

注意:桩函数不得修改被测代码的语义。若发现必须修改生产代码才能打桩,应优先考虑依赖注入或接口抽象,而不是在测试中“绕过”设计缺陷。

四、选型决策与定制要点

综合以上,航空嵌入式单元测试框架选型可归纳为:目标板认证测试选Unity+CMock,理由是零堆、纯C、证据链清晰;宿主机快速回归选GoogleTest,利用其丰富的匹配器与参数化测试;需要内存泄漏检测的C++模块选CppUTest。定制时重点检查三项:mock记录数组上限是否覆盖最坏调用次数、断言失败是否可定位到源文件行号、桩函数是否支持序列返回值。只有这三项在目标板上验证通过,框架才算真正可用。

航空嵌入式单元测试框架选型与定制 选型维度 • 语言支持(C/C++) • 是否依赖堆 • 断言粒度 • 桩/模拟能力 • 覆盖率集成 • 认证证据可追溯 Unity + CMock 纯C · 零堆 · 目标板 认证测试首选 GoogleTest C++ · 依赖堆/异常 宿主机快速回归 CppUTest C++ · 内存泄漏检测 C++模块回归 定制检查项 ① mock记录数组上限 CMOCK_MOCK_MAX_CALLS ≥ 最坏调用次数 ② 断言失败定位 文件+行号+表达式,可追溯 ③ 序列桩返回值 _StubWithCallback 按次数返回 ④ 镜像RAM预算 .bss + .data ≤ 目标板限制 断言与桩函数机制 断言宏 TEST_ASSERT_FLOAT_WITHIN(0.001f,...) 桩模式 ExpectAndReturn / StubWithCallback 验证闭环 调用次数+参数+返回值三重校验

最后强调一点:框架只是工具,真正的质量来自测试用例对需求的覆盖。选型完成后,应先用一个真实模块(如ADC采集)跑通“断言—桩—覆盖率”全链路,确认目标板运行稳定、失败可定位、证据可归档,再推广到全项目。这样既能避免后期返工,也能让单元测试真正成为航空软件适航证据的一部分。