3、第一个TDD实践:红阶段、绿阶段、重构阶段
好,咱们直接动手吧。
前面聊了那么多理论,说实话,不写代码都是白搭。这一章,我带你把TDD的完整流程走一遍。你会看到红、绿、重构这三个阶段到底是怎么玩的。
我选了一个非常经典的例子:判断一个年份是否为闰年。简单,但足够说明问题。
3.1 需求先搞清楚
先别急着写代码。咱们得知道要测什么。
闰年的规则其实不复杂:
- 能被4整除,但不能被100整除
- 或者,能被400整除
举个例子:2000年是闰年,1900年不是,2024年是,2023年不是。
嗯,规则清楚了。那咱们开始写测试。
3.2 红阶段:先写一个会失败的测试
这是TDD的铁律——没有测试,就不写代码。
我习惯先创建一个项目目录,然后初始化npm:
mkdir leap-year-tdd
cd leap-year-tdd
npm init -y
接着安装一个测试框架。我个人偏爱Jest,用起来顺手:
npm install jest --save-dev
在 package.json 里加上测试脚本:
"scripts": {
"test": "jest"
}
好,现在写第一个测试文件 isLeapYear.test.js:
const isLeapYear = require('./isLeapYear');
test('2000年是闰年', () => {
expect(isLeapYear(2000)).toBe(true);
});
你想想看,这时候 isLeapYear.js 还不存在呢。运行 npm test 会怎样?
结果:测试失败。 提示找不到模块。这就是红阶段。
为什么要先让它失败?说白了,就是确认你的测试本身没问题。如果测试一开始就通过了,你反而要怀疑——是不是测试写错了?
3.3 绿阶段:写最简代码让测试通过
现在,咱们的目标只有一个:让测试变绿。
别想太多,别过度设计。写最少的代码就行。
创建 isLeapYear.js:
function isLeapYear(year) {
return true;
}
module.exports = isLeapYear;
你没看错。就一行 return true。
运行测试:
npm test
测试通过了! 绿了。
你可能会觉得这代码太蠢了。没错,它就是蠢。但TDD的精髓就是:先让测试通过,再优化代码。你现在写的代码只是临时方案,后面会重构的。
3.4 继续加测试,继续红绿循环
一个测试不够。咱们再加一个:
test('1900年不是闰年', () => {
expect(isLeapYear(1900)).toBe(false);
});
运行测试——红了。因为 return true 显然不能处理1900年。
好,现在修改代码:
function isLeapYear(year) {
if (year % 400 === 0) return true;
if (year % 100 === 0) return false;
return year % 4 === 0;
}
再跑测试——绿了。
你看,每一步都很小。每次只加一个测试,然后写刚好能通过的代码。我在项目中遇到过很多次,就是因为步子迈太大,结果一堆bug找都找不到。
3.5 重构阶段:优化代码结构
测试都通过了,代码也工作了。但你看现在的代码,是不是有点啰嗦?
重构阶段的目标是:不改变功能,只优化结构。
我个人习惯把条件合并一下:
function isLeapYear(year) {
return (year % 400 === 0) || (year % 4 === 0 && year % 100 !== 0);
}
简洁多了,对吧?
重构完一定要再跑一次测试——确保还是绿的。这是底线。
注意: 重构时不要添加新功能。只改结构,不改行为。我曾经有一次重构时顺手加了个新特性,结果测试全崩了……从那以后,我重构时绝对不动业务逻辑。
3.6 完整示例演示
来,我把整个流程串起来给你看一遍。
最终代码:
// isLeapYear.js
function isLeapYear(year) {
return (year % 400 === 0) || (year % 4 === 0 && year % 100 !== 0);
}
module.exports = isLeapYear;
// isLeapYear.test.js
const isLeapYear = require('./isLeapYear');
test('2000年是闰年', () => {
expect(isLeapYear(2000)).toBe(true);
});
test('1900年不是闰年', () => {
expect(isLeapYear(1900)).toBe(false);
});
test('2024年是闰年', () => {
expect(isLeapYear(2024)).toBe(true);
});
test('2023年不是闰年', () => {
expect(isLeapYear(2023)).toBe(false);
});
运行结果:
PASS ./isLeapYear.test.js
✓ 2000年是闰年 (2 ms)
✓ 1900年不是闰年 (1 ms)
✓ 2024年是闰年 (1 ms)
✓ 2023年不是闰年 (1 ms)
四个测试,全部通过。
3.7 总结一下这个流程
| 阶段 | 做什么 | 状态 |
|---|---|---|
| 红 | 写一个测试,运行,确认失败 | 测试红色 |
| 绿 | 写最简代码,让测试通过 | 测试绿色 |
| 重构 | 优化代码,不改变功能 | 保持绿色 |
说白了,TDD就是让你小步快跑。每一步都稳,每一步都验证。你想想看,如果每次改动都有测试兜底,你还怕改代码吗?
我的建议: 刚开始练TDD时,别贪多。一天就练一个函数,反复走红绿重构循环。等你习惯了这种节奏,再应用到真实项目中去。
好,这一章就到这儿。下一章咱们会聊更复杂的场景——异步代码怎么测。到时候见。