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的单元测试环境,包括工具链的选择和配置。到时候我会手把手带大家走一遍。