一、单元测试基础概念
什么是单元测试
单元测试,说白了就是给代码做「最小粒度的体检」。
我习惯这样定义它:单元测试是针对软件中最小可测试单元(函数、方法、模块)的验证活动。在嵌入式世界里,这个「最小单元」通常是一个 C 函数、一个静态方法,或者一个中断服务例程。
举个例子,你写了一个计算 CRC 校验的函数:
uint16_t calc_crc16(const uint8_t *data, uint32_t len);
单元测试要做的就是:给它一组已知输入,检查输出是不是你期望的 CRC 值。就这么简单。
核心特征:单元测试是白盒的、自动化的、可重复的。它不依赖硬件,不依赖外设,甚至不依赖操作系统。
为什么需要单元测试
你可能觉得:「我代码跑起来没问题啊,干嘛多此一举?」
嗯,我以前也这么想。直到有一次,我在一个电机控制项目里改了一行滤波参数,结果三天后才发现另一处逻辑被带偏了。排查花了两天,改回来只花了五分钟。
单元测试的价值,我总结为四点:
- 尽早发现缺陷 —— 编码阶段就能揪出逻辑错误,不用等到集成时炸锅
- 降低修复成本 —— 缺陷发现越晚,修复代价越高,这是铁律
- 保障重构安全 —— 你敢不敢改一段三年前的代码?有单元测试在,你就敢
- 形成可执行文档 —— 测试用例本身就是最好的使用说明
我的经验:嵌入式项目里,硬件环境越复杂,单元测试的回报率越高。你想想看,在硬件还没回来的时候就能验证大部分逻辑,这能省多少时间?
单元测试 vs 集成测试
很多人分不清这两个概念。我直接画个对比表,一目了然:
| 对比维度 | 单元测试 | 集成测试 |
|---|---|---|
| 测试对象 | 单个函数/模块 | 多个模块的交互 |
| 执行环境 | 主机(PC) | 目标板或仿真器 |
| 依赖关系 | 可隔离(打桩/模拟) | 真实依赖 |
| 执行速度 | 毫秒级 | 秒级甚至分钟级 |
| 发现问题类型 | 逻辑错误、边界条件 | 接口不匹配、时序问题 |
| 谁来做 | 开发工程师 | 测试工程师或集成工程师 |
说白了,单元测试管的是「每个零件合不合格」,集成测试管的是「零件装在一起能不能转」。两个都重要,但顺序不能乱。
我曾经踩过的坑:在一个车载项目中,团队跳过单元测试直接做集成测试。结果每次集成都炸,定位问题要花大量时间。后来补上单元测试,集成一次通过的几率从 30% 提升到了 85%。
嵌入式单元测试的特殊性
嵌入式软件做单元测试,跟纯软件项目不太一样。我列几个关键点:
- 硬件耦合 —— 寄存器操作、中断、外设访问,这些都需要打桩
- 资源受限 —— 目标板上跑不了测试框架,得在 PC 上交叉编译测试
- 实时性要求 —— 有些逻辑跟时间强相关,测试时要模拟时序
- 编译器差异 —— 主机编译器跟交叉编译器行为可能不同
你想想看,一个操作寄存器的函数,在目标板上跑没问题,但在 PC 上编译就报错。怎么办?这就需要我们做「硬件抽象层」的打桩处理。这部分后面章节会详细讲。
本章知识体系
下面这张图,把单元测试的核心概念串起来了:
什么时候开始做单元测试
我的建议是:从项目第一天就开始。
别等到代码写完了再补测试,那时候你根本没动力。我个人的习惯是:每写完一个函数,顺手就把它的测试用例写了。花不了几分钟,但后面能省大把时间。
实用技巧:刚开始不用追求 100% 覆盖率。先把核心逻辑、边界条件、异常分支覆盖到,剩下的慢慢补。80% 的覆盖率比 0% 强一百倍。