第2章 测试驱动开发(TDD)基础
测试驱动开发,圈里人常说的TDD,不是什么高深莫测的玄学。说白了就是「先写测试,再写代码」。我第一次接触这个概念时,心里也犯嘀咕:代码都没写,测什么呢?后来真正上手才明白——这恰恰是TDD最精妙的地方。
2.1 红-绿-重构循环
TDD的核心流程,可以浓缩成三个步骤。我习惯叫它「红绿灯法则」。
- 红灯:先写一个会失败的测试。这时候代码还没实现,测试跑不过,显示红色。
- 绿灯:写最简代码让测试通过。别想太多,能过就行。
- 重构:在测试保护下,优化代码结构。测试还在,你改起来心里有底。
这个循环有多快?我建议你控制在3-5分钟内转一圈。超过这个时间,说明步子迈大了。
核心要点:红-绿-重构不是三个独立阶段,而是一个持续迭代的节奏。每次只加一个功能点,每次只改一个测试用例。
来看个嵌入式场景的例子。假设你要写一个LED闪烁控制函数:
// 第一步:写测试(红灯)
void test_led_blink_should_toggle_state(void) {
led_init();
uint8_t initial = led_get_state();
led_blink();
uint8_t after = led_get_state();
TEST_ASSERT_NOT_EQUAL(initial, after);
}
// 第二步:写最简实现(绿灯)
void led_blink(void) {
// 先写个假的,让测试通过
led_set_state(!led_get_state());
}
// 第三步:重构(优化实现)
void led_blink(void) {
static uint8_t toggle = 0;
toggle = !toggle;
led_set_state(toggle);
}
嗯,这里要注意:重构阶段不能加新功能。只改结构,不改行为。我在项目中见过有人重构时顺手加了个参数,结果测试全崩了——这就是没守住边界。
2.2 TDD的优势与挑战
先说说好处。我做了十几年嵌入式开发,TDD给我最大的感受是:安全感。
| 优势 | 具体表现 | 我的体会 |
|---|---|---|
| 即时反馈 | 写完代码就知道对不对 | 不用等到烧录才发现bug |
| 设计驱动 | 测试倒逼你写出可测的接口 | 代码耦合度自然降低 |
| 回归保障 | 改代码不怕破坏旧功能 | 重构时心里踏实 |
| 文档即代码 | 测试用例就是活文档 | 新人上手快很多 |
但TDD不是万能药。尤其在我们嵌入式领域,挑战很现实:
- 硬件依赖:GPIO、UART这些外设,测试环境里没有实物。我曾经在一个项目中,为了测一个SPI驱动,不得不先写一套模拟器——那工作量,够呛。
- 实时性要求:中断处理、时序敏感代码,用TDD很难覆盖。你想想看,一个1ms的中断响应,测试框架本身就有延迟,怎么测准?
- 资源限制:MCU的RAM和Flash就那么点,跑测试框架可能都费劲。我遇到过STM32F0系列,8KB RAM,连Unity测试框架都塞不进去。
- 团队习惯:很多嵌入式工程师习惯「写完再测」,改到TDD模式需要时间。我刚开始推TDD时,团队里有人说「这不是浪费时间吗」——后来他们真香了。
避坑指南:我曾经在一个实时操作系统项目中强行推行TDD,结果中断处理函数的测试覆盖率不到20%。后来我学乖了——TDD适合业务逻辑层,底层驱动和中断处理用传统方法更靠谱。
2.3 TDD在嵌入式中的适用性
那么问题来了:嵌入式开发到底适不适合TDD?我的答案是:分层次,看场景。
我习惯把嵌入式软件分成三层:
- 应用层:业务逻辑、状态机、协议解析。这部分和硬件解耦,TDD效果最好。
- 中间层:驱动封装、任务调度、内存管理。需要做接口抽象,TDD可行但成本高。
- 硬件层:寄存器操作、中断服务、时序控制。TDD基本不适用,靠硬件调试。
你想想看,一个温控系统的逻辑判断,完全可以在PC上跑测试。但一个PWM波形生成,必须上示波器看。
我的建议:从应用层开始练手。先写几个状态机测试,感受一下「红-绿-重构」的节奏。等熟练了,再往中间层推进。别一上来就挑战硬件层——那是在给自己挖坑。
下面这张图,是我总结的TDD在嵌入式中的适用性框架:
这张图我用了很多年。每次给团队做技术分享,我都会指着它说:别想着全盘TDD,那是自找麻烦。找准自己的位置,从绿色区域开始。
最后说一句:TDD不是银弹,但它确实能帮你写出更靠谱的代码。我在一个车载项目中,用TDD把协议栈的bug率从每千行5个降到了0.3个——当然,那是在应用层。硬件层该踩的坑,一个都没少。