2. TDD核心概念:红-绿-重构循环、单元测试定义、测试覆盖率、FIRST原则

好,咱们正式开始啃TDD的核心概念。这一章是整门课的基石,说白了,后面所有实战都建立在这些概念之上。我见过不少团队,工具用得挺溜,但概念没吃透,写着写着就变味了。咱们今天就把这几个关键点掰开揉碎,讲清楚。

2.1 红-绿-重构循环:TDD的节奏感

TDD最核心的,就是这个红-绿-重构循环。它不是一个死板的流程,而是一种节奏。你想想看,写代码最怕什么?最怕写着写着就不知道自己在干嘛了。这个循环就是给你一个节拍器,让你每一步都走得踏实。

红-绿-重构三步走:

  1. 红(Red):先写一个会失败的测试。注意,是先写测试,再写代码。这个测试描述了你要加的一个小功能,或者你要修的一个小bug。运行它,必须是红的(失败)。
  2. 绿(Green):写刚好能让测试通过的代码。别多想,别过度设计,怎么简单怎么来。目标是让红灯变绿灯。
  3. 重构(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 知识体系总览

下面这张图,把这一章的核心概念串起来了。你可以把它当作一个思维导图,方便以后回顾。

TDD核心概念知识体系 TDD核心概念 红-绿-重构循环 单元测试定义 测试覆盖率 FIRST原则 写失败测试 写通过代码 优化重构 隔离外部依赖(Mock) 只测逻辑,不测硬件 行/分支/函数覆盖率 关注信心,而非数字 Fast, Isolated, Repeatable Self-validating, Timely

嗯,这一章的内容就这些。概念虽然多,但核心就是一句话:用测试来驱动你的设计,而不是用测试来验证你的代码。记住这个区别,后面的路就好走了。