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时,别贪多。一天就练一个函数,反复走红绿重构循环。等你习惯了这种节奏,再应用到真实项目中去。

好,这一章就到这儿。下一章咱们会聊更复杂的场景——异步代码怎么测。到时候见。