1. 单元测试基础概念

各位同学好,我是这门课的主讲。今天咱们聊聊单元测试最基础的东西。说实话,我见过太多嵌入式工程师一听到「测试」两个字就皱眉头,觉得那是测试组的事。但做了十几年嵌入式开发,我越来越觉得——单元测试不是负担,而是救命的工具。

什么是单元测试

单元测试,说白了就是把你代码里最小的可测试单元拿出来,单独验证它对不对。

什么叫「最小可测试单元」?在C语言里,通常就是一个函数。比如你写了个计算CRC校验的函数,单元测试就是单独测这个函数——给它各种输入,看它输出对不对。

核心要点:单元测试关注的是「这个函数本身有没有bug」,而不是「这个函数和其他函数配合起来有没有问题」。

举个例子,假设你有这样一个函数:

// 计算两个整数的最大公约数
int gcd(int a, int b) {
    while (b != 0) {
        int temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}

单元测试会这样测:

void test_gcd(void) {
    assert(gcd(12, 8) == 4);   // 正常情况
    assert(gcd(7, 13) == 1);   // 互质
    assert(gcd(0, 5) == 5);    // 边界:0
    assert(gcd(-6, 9) == 3);   // 负数
}

嗯,这里要注意——测试用例要覆盖正常情况、边界情况、异常情况。我在项目中遇到过有人只测了正常情况,结果上线后遇到负数输入直接崩了。

为什么需要单元测试

这个问题我问过很多工程师。有人说「浪费时间」,有人说「项目紧没空」。但我想反问一句:你花在调试上的时间,真的比写测试少吗?

我个人习惯是:写完一个函数,先写测试再集成。为什么?因为bug发现得越早,修起来越便宜。你想想看:

  • 单元测试阶段发现bug:改几行代码,重新编译,几秒钟搞定
  • 集成测试阶段发现bug:要定位是哪个模块的问题,可能得花半天
  • 产品上线后才发现bug:OTA升级、客户投诉、紧急修复……成本翻几十倍

我的经验:嵌入式项目里,70%以上的bug其实都是逻辑错误,而不是硬件问题。而这些逻辑错误,绝大多数在单元测试阶段就能暴露出来。

还有一点很多人忽略——单元测试是最好的文档。你写的测试用例,就是告诉后来人「这个函数应该怎么用、边界在哪」。我曾经接手过一个项目,代码注释几乎没有,但测试用例写得特别全。说实话,看测试比看代码更能理解设计意图。

单元测试与集成测试的区别

这两个概念经常被混淆。我画个简单的对比:

维度 单元测试 集成测试
测试对象 单个函数/模块 多个模块的交互
测试环境 可以在PC上运行 通常需要目标硬件
执行速度 毫秒级 秒级甚至分钟级
发现问题 逻辑错误、边界问题 接口不匹配、时序问题
依赖处理 需要mock/stub隔离 尽量真实依赖

说白了,单元测试是「自底向上」的验证,集成测试是「模块间握手」的验证。两者不是替代关系,而是互补关系。

我记得有一次,我们团队做的一个传感器驱动,单元测试全过了,但集成到系统里就是读不到数据。查了半天,发现是I2C通信的时序参数在单元测试里被mock掉了,实际硬件上时序不满足要求。这就是典型的「单元测试过了,集成测试才能发现问题」的场景。

嵌入式C单元测试的特殊性

做嵌入式C的单元测试,和做纯软件单元测试,差别还是挺大的。我总结了几点:

1. 硬件依赖

嵌入式代码经常直接操作寄存器、中断、外设。这些在PC上没法直接跑。怎么办?

  • 用模拟器(比如QEMU)
  • 用硬件抽象层(HAL)隔离
  • 用mock/stub模拟硬件行为

我个人比较推荐第二种——设计时就考虑可测试性,把硬件操作封装成接口。这样测试时只需要替换底层实现就行。

2. 资源受限

嵌入式系统内存小、CPU慢。你不能在目标板上跑一套完整的测试框架。我的做法是:

  • 在开发机上编译测试
  • 用交叉编译器生成目标代码
  • 在模拟器或开发板上运行

注意:在PC上通过的测试,不代表在目标板上也能通过。字节序、数据类型长度、编译器优化等差异,都可能引入新问题。

3. 实时性要求

有些嵌入式系统对时间要求很严格。单元测试里你没法模拟真实的时序。我曾经遇到过一个问题:一个中断处理函数在单元测试里跑得好好的,但实际系统中因为中断嵌套导致栈溢出。这种问题,单元测试很难覆盖到。

4. 编译器差异

嵌入式开发常用Keil、IAR、GCC等不同编译器。每个编译器的行为略有不同。比如:

// 这段代码在不同编译器下行为可能不同
volatile int flag = 0;
if (flag) {
    // 某些编译器可能优化掉这个判断
}

所以我的建议是:单元测试至少要在目标编译器上跑一遍。

5. 测试框架选择

嵌入式C的测试框架有不少,比如:

  • CUnit:轻量级,适合资源受限环境
  • Unity:专门为嵌入式设计的
  • CMock:配合Unity做mock
  • Ceedling:集成了构建和测试

我个人习惯用Unity + CMock的组合。Unity足够轻,CMock能帮我自动生成桩代码,省了不少事。

总结一下:嵌入式C单元测试不是不能做,而是需要针对嵌入式特点做调整。核心思路就是——把硬件依赖剥离出去,让测试逻辑和硬件解耦。这样你才能在PC上快速验证代码逻辑的正确性。

好了,这一章就到这里。下一章我会讲怎么搭建嵌入式C的单元测试环境,包括工具链的选择和配置。到时候我会手把手带大家走一遍。