2. TDD核心概念:红-绿-重构循环、单元测试定义、测试覆盖率、FIRST原则
好,咱们正式开始啃TDD的核心概念。这一章是整门课的基石,说白了,后面所有实战都建立在这些概念之上。我见过不少团队,工具用得挺溜,但概念没吃透,写着写着就变味了。咱们今天就把这几个关键点掰开揉碎,讲清楚。
2.1 红-绿-重构循环:TDD的节奏感
TDD最核心的,就是这个红-绿-重构循环。它不是一个死板的流程,而是一种节奏。你想想看,写代码最怕什么?最怕写着写着就不知道自己在干嘛了。这个循环就是给你一个节拍器,让你每一步都走得踏实。
红-绿-重构三步走:
- 红(Red):先写一个会失败的测试。注意,是先写测试,再写代码。这个测试描述了你要加的一个小功能,或者你要修的一个小bug。运行它,必须是红的(失败)。
- 绿(Green):写刚好能让测试通过的代码。别多想,别过度设计,怎么简单怎么来。目标是让红灯变绿灯。
- 重构(Refactor):测试通过了,代码安全了。现在,在绿灯的保护下,大胆地优化代码。去掉重复,改善命名,调整结构。重构完,再跑一遍测试,确保还是绿的。
这个循环有多短?我个人习惯,一个循环控制在5分钟以内。如果超过10分钟还没变绿,说明步子迈大了,得拆小点。
我曾经在一个嵌入式项目中,需要写一个温度传感器的驱动。按照老习惯,我肯定先把I2C通信、寄存器配置、数据转换全写完,再开始测。结果呢?一测全是bug,根本不知道是通信问题还是算法问题。后来我用TDD,先写一个“读取原始寄存器值”的测试,让它红,然后写一行读寄存器的代码,让它绿,重构。再写一个“将原始值转换为摄氏度”的测试... 每一步都清清楚楚,心里特别有底。
我的小技巧:在“绿”这一步,允许自己写“脏代码”。比如直接返回一个硬编码的值让测试通过。这听起来很蠢,但能让你快速进入“重构”阶段,用测试来验证你的重构是否正确。这叫“三角测量法”,后面会细讲。
2.2 单元测试定义:到底测什么?
很多人对单元测试有误解,觉得就是测函数。其实没那么简单。单元测试的核心是隔离。你测的是一个“单元”,这个单元通常是一个函数、一个方法、或者一个模块。关键是,这个单元要能独立运行,不依赖外部环境(比如硬件、数据库、网络)。
在嵌入式世界里,这一点尤其重要。你的代码跑在MCU上,依赖GPIO、UART、定时器。如果直接测,你得烧录、跑起来、看波形,太慢了。单元测试就是要把这些依赖都Mock(模拟)掉,只测你写的逻辑。
| 特性 | 单元测试 | 集成测试 | 系统测试 |
|---|---|---|---|
| 测试范围 | 单个函数/模块 | 多个模块交互 | 整个系统 |
| 执行速度 | 毫秒级 | 秒级 | 分钟/小时级 |
| 依赖 | 无(Mock掉) | 部分真实依赖 | 全部真实环境 |
| 定位问题 | 精准到行 | 模块间 | 系统级 |
| 维护成本 | 低 | 中 | 高 |
举个例子,你要测一个“计算PWM占空比”的函数。这个函数输入是目标温度,输出是占空比数值。它不涉及任何硬件操作,就是一个纯算法。这就是一个完美的单元测试对象。你给它输入0度、25度、100度,看它返回的占空比对不对就行。
避坑指南:我曾经见过有人把整个驱动层写成一个巨大的函数,然后说“没法单元测试”。其实不是没法测,是设计没做好。好的设计天然就是可测试的。如果你发现一个函数很难写单元测试,那往往是设计需要重构的信号。
2.3 测试覆盖率:数字不是目标,信心才是
测试覆盖率,通常指代码覆盖率,就是你的测试跑了多少行代码。工具会告诉你:行覆盖率、分支覆盖率、函数覆盖率等等。很多公司会定KPI,比如“行覆盖率必须达到80%”。
我个人觉得,这有点本末倒置。覆盖率是一个发现盲区的工具,不是一个考核指标。你想想看,如果你为了凑80%的覆盖率,写了一堆“断言1+1=2”这种毫无意义的测试,那覆盖率再高也没用。
我建议你这样看覆盖率:
- 低覆盖率(< 50%): 说明你的代码大部分没被测试过,风险很高。赶紧补测试。
- 中等覆盖率(50% - 80%): 核心逻辑应该都覆盖了,但边界条件和异常处理可能漏了。需要检查一下。
- 高覆盖率(> 80%): 说明你测得很全面了。但别高兴太早,看看那20%没覆盖的代码是什么。如果是错误处理、边界条件,那还是得补上。
真正重要的,是你对你写的代码有没有信心。当你改了一行代码,跑完所有测试,全部通过,你敢不敢直接提交?如果敢,那你的测试就是有效的。如果不敢,那覆盖率再高也是虚的。
2.4 FIRST原则:好测试的五个标准
不是所有测试都是好测试。我见过有人写的测试,跑一次要10分钟,而且今天能过明天就挂,根本不知道原因。这种测试,还不如不写。好的单元测试,应该遵循FIRST原则。
| 字母 | 含义 | 解释 | 反例 |
|---|---|---|---|
| F | Fast(快速) | 测试要快,毫秒级。慢了你就懒得跑了。 | 测试里调用了sleep(1000) |
| I | Isolated(隔离) | 测试之间互不影响。一个测试失败,不影响其他测试。 | 测试A修改了全局变量,测试B依赖这个变量 |
| R | Repeatable(可重复) | 在任何环境、任何时候跑,结果都一样。 | 测试依赖当前系统时间或随机数 |
| S | Self-validating(自验证) | 测试结果只有“通过”或“失败”,不需要人工看日志。 | 测试只打印结果,需要人眼去判断 |
| T | Timely(及时) | 测试应该在生产代码之前写(TDD的核心)。 | 代码写完了再补测试 |
这五个原则,我建议你贴在工位上。每次写完一个测试,对照着检查一遍。尤其是隔离性,在嵌入式测试里特别容易踩坑。比如你Mock了一个全局的硬件状态寄存器,结果两个测试用例都用了同一个Mock对象,状态互相污染了。这种bug查起来特别头疼。
我的经验:为了做到“快速”,我会把单元测试和硬件解耦。在PC上用GCC编译,用Cmock或Unity这些轻量级框架跑测试。这样一次跑几百个测试,也就几秒钟。如果每次都要烧录到开发板上跑,那速度就完全没法接受了。
2.5 知识体系总览
下面这张图,把这一章的核心概念串起来了。你可以把它当作一个思维导图,方便以后回顾。
嗯,这一章的内容就这些。概念虽然多,但核心就是一句话:用测试来驱动你的设计,而不是用测试来验证你的代码。记住这个区别,后面的路就好走了。