单元测试基础理论
好,咱们正式开始聊单元测试的基础理论。这部分内容,说白了就是地基。地基打不牢,后面盖再高的楼也悬。我个人习惯,每接手一个新项目,第一件事不是看代码,而是先想清楚:这个模块的单元测试该怎么测?
单元测试的定义
单元测试,听起来挺唬人,其实没那么复杂。它就是对软件中的最小可测试单元进行检查和验证。在嵌入式世界里,这个“最小单元”通常是一个函数,或者一个模块的接口。
我举个例子你就明白了。比如你写了一个计算CRC校验的函数:
uint32_t calculate_crc(uint8_t *data, uint32_t len);
单元测试要做的,就是给这个函数喂不同的数据,看它吐出来的CRC值对不对。就这么简单。但为什么很多人做不好?因为边界条件没想全。
核心要点:单元测试不是测试整个系统,而是测试一个独立的功能点。它关注的是“这个函数对不对”,而不是“整个系统跑不跑得起来”。
我在项目中遇到过一件事:有个同事写了个温度传感器驱动,单元测试全过了,结果一上硬件就崩。后来一查,问题出在I2C通信的超时处理上——他的单元测试根本没覆盖到总线挂死的情况。你看,这就是单元测试没做到位。
测试金字塔
测试金字塔这个概念,我觉得每个嵌入式工程师都应该刻在脑子里。它长这样:
| 层级 | 名称 | 数量 | 执行速度 | 维护成本 |
|---|---|---|---|---|
| 顶层 | 端到端测试 | 少 | 慢 | 高 |
| 中间层 | 集成测试 | 适中 | 中等 | 中等 |
| 底层 | 单元测试 | 多 | 快 | 低 |
为什么叫金字塔?因为底层的单元测试应该最多,越往上越少。你想想看,如果每个函数都测好了,集成的时候出问题的概率是不是就小很多?
我曾经在一个医疗内窥镜项目上吃过亏。团队为了赶进度,单元测试写得很少,直接跳到集成测试。结果呢?每次集成都要花两三天去定位问题,最后发现都是些低级错误——指针没判空、数组越界这些。如果单元测试做扎实了,这些bug在写代码当天就能发现。
我的建议:单元测试、集成测试、端到端测试的比例,我一般按7:2:1来分配。70%的精力放在单元测试上,这是性价比最高的投入。
白盒测试与黑盒测试
这两个概念,很多新手容易搞混。我换个说法你就懂了。
白盒测试
白盒测试,说白了就是“打开盒子看里面”。你清楚代码的内部逻辑,知道每个分支、每个循环、每个条件判断。测试的时候,你就盯着这些内部结构去设计用例。
比如这个函数:
int8_t check_battery_level(uint16_t voltage) {
if (voltage > 4200) {
return BATTERY_OVER_VOLTAGE;
} else if (voltage < 3000) {
return BATTERY_UNDER_VOLTAGE;
} else {
return BATTERY_NORMAL;
}
}
白盒测试会怎么测?它会覆盖三个分支:电压过高、电压过低、正常范围。还要测边界值:4200、3000、4199、3001这些点。嗯,这里要注意,边界值是最容易出bug的地方。
黑盒测试
黑盒测试就不一样了。你把这个函数当成一个黑盒子,不管里面怎么实现的,只看输入输出对不对。你给它4201,它返回BATTERY_OVER_VOLTAGE,这就对了。
黑盒测试的好处是什么?它不依赖代码实现。哪怕你明天把if-else改成switch-case,只要接口不变,测试用例就不用改。
| 对比项 | 白盒测试 | 黑盒测试 |
|---|---|---|
| 关注点 | 内部逻辑结构 | 外部功能行为 |
| 测试依据 | 源代码 | 需求规格说明 |
| 覆盖率 | 语句、分支、路径 | 等价类、边界值 |
| 适用阶段 | 单元测试 | 系统测试 |
避坑指南:我曾经犯过一个错误——只做白盒测试,不做黑盒测试。结果代码覆盖率100%,但功能就是不对。为什么?因为需求理解错了。白盒测试只能保证“代码按你的意图跑”,不能保证“你的意图是对的”。所以,两种测试要结合着来。
在实际的内窥镜项目中,我通常这样分配:单元测试阶段以白盒为主,黑盒为辅。白盒保证代码逻辑全覆盖,黑盒保证功能需求全满足。你想想看,一个图像处理函数,白盒能帮你发现数组越界,黑盒能帮你验证输出图像是不是清晰——两者缺一不可。
好了,单元测试的基础理论就聊到这儿。记住三句话:单元测试测的是最小单元,测试金字塔告诉我们精力怎么分配,白盒黑盒要双管齐下。下一章咱们聊聊具体的测试框架怎么选,到时候我会分享一些实际踩过的坑。