1. 课程导入
各位做嵌入式的朋友,咱们先聊点实在的。
你写了一段C代码,控制一个电机。编译通过,下载到板子上,电机转了。是不是就完事了?
我早年也这么想。直到有一次,电机在特定角度下突然抖动——查了三天,最后发现是一个边界条件没处理。从那以后,我养成了一个习惯:写代码之前,先想好怎么测它。
这就是单元测试的起点。
1.1 什么是单元测试
单元测试,说白了就是验证代码里最小的可测试单元。在嵌入式C语言里,这个单元通常是一个函数。
举个例子:
// 你要测的这个函数
int calculate_speed(int pulse_count, int time_ms) {
if (time_ms <= 0) return 0;
return (pulse_count * 1000) / time_ms;
}
单元测试就是写一段代码,专门去调用它,检查返回值对不对:
void test_calculate_speed_normal(void) {
int result = calculate_speed(100, 50);
// 期望:100 * 1000 / 50 = 2000
assert(result == 2000);
}
void test_calculate_speed_zero_time(void) {
int result = calculate_speed(100, 0);
// 期望:除零保护,返回0
assert(result == 0);
}
嗯,就是这么回事。每个测试用例只测一件事,测完就收工。
核心要点:单元测试不是集成测试,也不是系统测试。它只关心一个函数、一个模块在隔离环境下的行为。你不需要把整个硬件跑起来,只需要在PC或者模拟器上编译运行测试代码。
1.2 为什么需要单元测试
我经常被问到:嵌入式开发本来就慢,还要写测试,不是更慢吗?
我的回答是:你写测试花的时间,会在调试阶段加倍还给你。
来看几个真实场景:
- 场景一:你改了一行代码,编译下载,上板跑。结果系统卡死了。你花了两个小时定位,发现是另一个模块的bug。如果当时有单元测试,跑一遍就知道问题在哪。
- 场景二:项目做到一半,需求变了。你改了A函数,心里没底——会不会影响B函数?单元测试就是你的安全网。改完代码,跑一遍测试,全绿就放心了。
- 场景三:团队协作,你写的模块别人要用。没有测试,别人不敢动你的代码。有测试,别人改完跑一遍,就知道有没有破坏你的逻辑。
| 对比维度 | 没有单元测试 | 有单元测试 |
|---|---|---|
| 修改代码后的信心 | 低,全靠手动测试 | 高,自动化验证 |
| 回归测试成本 | 每次都要上板跑 | PC上几秒搞定 |
| bug定位时间 | 小时级甚至天级 | 分钟级 |
| 代码可维护性 | 越改越乱 | 持续重构有保障 |
我曾经踩过的坑:有一个项目,我负责底层驱动,同事负责应用层。我改了一个定时器初始化函数,自以为没问题。结果同事的应用层跑飞了。因为没有单元测试,我们俩互相甩锅了两天。后来加了单元测试,这种事再也没发生过。
1.3 单元测试在嵌入式开发中的价值
嵌入式开发和纯软件不一样。我们有硬件依赖,有实时性要求,有资源限制。很多人觉得单元测试在嵌入式里搞不起来。其实不然。
我总结了几点核心价值:
- 早期发现bug,降低修复成本
bug发现得越晚,修复成本越高。在编码阶段通过单元测试发现的bug,改几行代码就行。等集成测试才发现,可能要动架构。
- 解耦硬件依赖,提升开发效率
嵌入式代码经常依赖硬件寄存器、外设、中断。单元测试通过打桩(stub)和模拟(mock)技术,把这些依赖替换掉。你可以在没有硬件的情况下,把逻辑测透。
- 保障代码质量,建立重构信心
代码不是一次写好的。随着需求变化,你需要重构。没有单元测试,重构就是赌博。有单元测试,重构就是有安全网的体操。
- 形成可执行的文档
单元测试本身就是最好的文档。它告诉你:这个函数在什么输入下,应该有什么输出。比任何注释都准确。
我的建议:刚开始做单元测试,不要追求100%覆盖率。先挑核心模块、复杂逻辑、容易出bug的地方开始。比如状态机、协议解析、算法计算。慢慢来,比不做强。
下面这张图,展示了单元测试在整个嵌入式开发流程中的位置:
你看这个流程。单元测试卡在编码和集成之间。它就像一个守门员——代码质量不过关,别想往下走。
我个人的经验是:单元测试不是负担,而是投资。你投入的时间,会在项目后期成倍回报。尤其是嵌入式项目,硬件调试环境往往稀缺,几个人排队用一块开发板。如果能在PC上把逻辑测透,上板调试的时间能缩短一半以上。
一句话总结:单元测试让嵌入式开发从「盲人摸象」变成「心中有数」。它不解决所有问题,但它能解决最让人头疼的那一类——「改了一行代码,不知道哪里会炸」的问题。