一、单元测试基础:什么是单元测试、为什么需要单元测试、单元测试与集成测试的区别
各位同学好,我是你们这门课的老朋友。今天咱们来聊聊单元测试最基础的东西。
说实话,我刚开始做嵌入式开发那几年,对单元测试也是嗤之以鼻的。总觉得「代码能跑就行,测什么测」。直到有一次,一个简单的串口驱动让我在产线上熬了三个通宵……嗯,从那以后,我再也不敢小看单元测试了。
1.1 什么是单元测试
单元测试,说白了就是「测一小块代码」。这一小块,通常是一个函数、一个模块、一个类的方法。我们把它从整个项目里单独拎出来,给它输入,看它输出对不对。
举个例子,你写了一个计算校验和的函数:
// 计算 CRC16 校验和
uint16_t calc_crc16(const uint8_t *data, uint32_t len)
{
uint16_t crc = 0xFFFF;
for (uint32_t i = 0; i < len; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
单元测试要做的,就是单独测试这个函数。给它一组已知的数据,比如 data = {0x01, 0x02, 0x03},预期结果是 0xCAE0。如果跑出来不对,那问题就锁定在这个函数里,不用去怀疑调用方或者硬件。
核心要点:单元测试的「单元」是最小可测试单元。在 C 语言里,通常就是一个函数。它不依赖外部硬件、不依赖其他模块、不依赖数据库或文件系统。
1.2 为什么需要单元测试
这个问题我当年也问过。你想想看,我们做嵌入式开发的,哪个不是被项目进度追着跑?哪有时间写测试?
但后来我算了一笔账,发现不写单元测试的成本其实更高。我给大家列几个真实场景:
- 回归测试的噩梦:改了一个定时器参数,结果把 ADC 采集的逻辑搞崩了。这种「牵一发动全身」的事,我遇到过不下十次。
- 硬件依赖的坑:硬件工程师还在画板子,你代码已经写完了。没有单元测试,你怎么验证?只能干等。
- 边界条件的遗漏:我记得有一次,一个环形缓冲区的代码,在缓冲区满的时候写数据,直接覆盖了旧数据。这种边界问题,集成测试很难覆盖到。
我的建议:单元测试不是「额外的工作」,而是「节省时间的工作」。你花 1 小时写测试,可能省下后面 10 小时的调试时间。这笔账,怎么算都划算。
为什么会这样?因为单元测试能帮你做到三件事:
- 快速定位问题:测试失败了,你立刻知道是哪个函数出了问题。
- 安全重构:改完代码跑一遍测试,绿灯全亮,你就放心了。
- 文档即代码:测试用例本身就是最好的使用文档。新同事看你的测试代码,就知道这个函数该怎么用。
1.3 单元测试与集成测试的区别
很多初学者会把这两个概念搞混。我简单画个图,大家一看就明白了。
从这张图可以看得很清楚:
- 单元测试是「各个击破」,每个函数单独测。就像检查一辆车的每个零件是否合格。
- 集成测试是「联调联试」,把各个模块拼起来测。就像把零件组装成整车,然后上路跑一圈。
我再用一个表格给大家对比一下:
| 对比维度 | 单元测试 | 集成测试 |
|---|---|---|
| 测试对象 | 单个函数/模块 | 多个模块的组合 |
| 依赖管理 | 可 mock/stub 外部依赖 | 需要真实依赖 |
| 执行速度 | 毫秒级,可批量运行 | 秒级甚至分钟级 |
| 定位问题 | 精确到函数级别 | 只能定位到模块间 |
| 编写成本 | 较低,可自动化 | 较高,需要搭建环境 |
| 典型场景 | 算法函数、协议解析、校验计算 | 驱动与协议栈联调、任务调度 |
注意:千万不要把单元测试和集成测试对立起来。它们不是「二选一」的关系,而是「互补」的关系。我见过一些团队,只做集成测试不做单元测试,结果出了问题根本不知道是哪个函数导致的。反过来,只做单元测试不做集成测试,又可能漏掉模块间的交互问题。
我个人习惯的做法是:先写单元测试,把每个函数都测透。等单元测试全部通过后,再写集成测试,验证模块间的配合。这样分层测试,效率最高。
我曾经在一个项目中,因为赶进度跳过了单元测试,直接做集成测试。结果一个 SPI 驱动的问题,花了整整两天才定位到——原来是片选信号的时序差了 2 个时钟周期。如果当时写了单元测试,这个 bug 十分钟就能揪出来。
嗯,这里要注意:单元测试不是银弹。它解决的是「代码逻辑是否正确」的问题,解决不了「需求理解是否正确」的问题。但话说回来,如果连代码逻辑都不能保证正确,后面的测试都是空中楼阁。
一句话总结:单元测试是地基,集成测试是框架。地基不牢,框架再漂亮也是危楼。
公众号:蓝海资料掘金营,微信deep3321