Skip to content

2.1 TDD:先写测试的工匠精神

老王上周刚经历了一次"回归灾难":他修了一个密码验证的 Bug,但三天后发现注册功能挂了——因为修 Bug 时不小心改了共享逻辑。

"如果我有测试,这个回归 Bug 根本不会发生。"——这就是老王坚持 TDD 的原因。


❌ 不用 TDD vs ✅ 用 TDD

❌ 不用 TDD 的开发方式

写代码 → 运行 → 手动测试 → 发现 Bug → 修 Bug → 又引入新 Bug → 继续修...
  • 手动测试不全面,每次改代码都可能破坏之前的功能
  • 修 Bug 时容易引入回归(修了 A 坏了 B)
  • 不敢重构,因为不知道会改坏什么

✅ TDD 的开发方式

写测试(失败)→ 写代码(通过)→ 重构 → 写下一个测试 → 循环
  • 每个 Bug 只被发现一次(变成测试用例)
  • 重构有安全网(测试通过 = 没改坏)
  • 测试即文档(看测试就知道代码应该怎么用)

TDD 的核心:Red-Green-Refactor

这不是三个步骤,而是一个永远不停的循环

Red 红色
写一个失败的测试。
证明测试确实在验证某些东西。
Green 绿色
最少的代码让测试通过。
先让它工作,不管丑不丑。
Refactor 重构
改善代码设计,保持测试通过。
有测试保护,放心重构。

每个循环只要几分钟。一天下来,你可能跑几十个循环。


老王的实际操作

让我们看老王实现"字符串大写"功能的完整 TDD 过程:

第一步:Red——写失败测试

typescript
// string-utils.test.ts
import { describe, it, expect } from 'vitest'
import { capitalize } from './string-utils'

describe('capitalize', () => {
  it('should capitalize first letter', () => {
    expect(capitalize('hello')).toBe('Hello')  // ← 功能还不存在
  })
})

运行测试 → 失败! 因为 capitalize 函数根本不存在。

第二步:Green——写最少的代码

typescript
// string-utils.ts
export function capitalize(str: string): string {
  return 'Hello'  // 硬编码——最简单的让测试通过的方式
}

运行测试 → 通过! 但这显然不对。

第三步:加新测试,再次 Red

typescript
it('should work with any word', () => {
  expect(capitalize('world')).toBe('World')  // ← 硬编码会失败
})

运行测试 → 失败! 迫使你写真正的实现:

typescript
export function capitalize(str: string): string {
  if (str.length === 0) return ''
  return str.charAt(0).toUpperCase() + str.slice(1)
}

运行测试 → 全部通过!

第四步:Refactor——优化

测试通过后,老王安心重构,因为任何错误都会被测试捕获。


为什么 TDD 有效?

这不是玄学——有硬数据支持:

40-90%
缺陷密度降低
来源:Nagappan et al. 2008,Empirical SE 期刊,Microsoft/IBM 四团队研究
15-35%
初始开发时间增加
但长期维护成本显著降低
AAA
Arrange-Act-Assert
每个测试的标准结构
xUnit
测试框架家族
JUnit、pytest、Jest、Vitest……

TDD 的暗面

TDD 不是万能的。老王也承认有些场景 TDD 不划算:

场景为什么不适合
探索性原型需求还不清楚,测什么?
快速变化的需求测试改了又改,成本太高
UI/视觉测试界面布局用单元测试很难覆盖
一次性脚本不值得投入

还有三个常见的反模式要避免:

  1. 过度 Mock——Mock 了所有依赖,测试通过了但实际运行还是挂
  2. 测试巨人——一个测试里 15 个断言,出错了不知道哪一步失败
  3. 说谎者测试——测试看着通过了,其实什么都没验证(异步代码常见)

2025-2026:AI 时代的 TDD

有趣的是,AI 不但没有杀死 TDD,反而让它更强了

模式描述
人写测试,AI 写实现你写测试定义"正确性",AI 生成代码通过测试
AI 帮你重构Refactor 阶段,AI 比人更擅长发现优化点
AI Agent 式 TDD工具自动运行测试 → 看失败信息 → 迭代修复 → 循环

最受欢迎的模式是**"人写测试,AI 写实现"**——保持人类对"正确性"的判断力,同时享受 AI 的速度。


📌 本节核心要点

概念要点
TDD 核心循环Red(写失败测试)→ Green(让测试通过)→ Refactor(重构)
实证数据缺陷密度降低 40-90%,初始开发时间增加 15-35%
数据来源Nagappan et al. 2008,Empirical SE 期刊,Microsoft/IBM 四团队研究
适用场景核心业务逻辑、长期维护项目、安全关键系统
不适用探索性原型、快速变化需求、一次性脚本
AI 时代最佳实践人类写测试,AI 写实现

知识检查

问题 1:TDD 的 Red-Green-Refactor 循环中,"Red"阶段为什么要"确认测试失败"?

查看答案

因为如果测试一开始就通过了,说明测试什么都没验证——可能是测试写错了,也可能是测试了一个已经存在的功能。确认失败 = 确认测试确实在测试某些东西

问题 2:TDD 实证研究(Nagappan et al. 2008)的核心发现是什么?数据来源是什么?

查看答案
  • 核心发现:使用 TDD 的团队,缺陷密度降低 40-90%(涵盖 3 个 Microsoft 团队和 1 个 IBM 团队),但初始开发时间增加 15-35%
  • 数据来源:Nagappan, Maximilien, Bhat, Williams 的论文,发表在 Empirical Software Engineering 期刊(不是 ICSE)
  • 关键结论:短期投入更多,长期回报更大

问题 3:以下哪些场景适合用 TDD?哪些不适合?

  • A. 银行支付系统的核心计算逻辑
  • B. 一次性数据清洗脚本
  • C. 刚开始探索新框架的实验项目
  • D. 长期维护的用户认证模块
查看答案
  • A. 适合 — 金融系统核心逻辑必须有测试保障
  • B. 不适合 — 一次性脚本不值得投入 TDD
  • C. 不适合 — 需求不明确,快速实验更重要
  • D. 适合 — 长期维护的认证模块需要回归测试保护

问题 4:TDD 有哪三个常见的反模式?分别是什么问题?

查看答案
  1. 过度 Mock:Mock 了所有依赖,测试通过了但实际运行还是挂
  2. 测试巨人:一个测试里 15+ 个断言,出错了不知道哪一步失败
  3. 说谎者测试:测试看着通过了,其实什么都没验证(异步代码常见)

下一节预告

TDD 从微观层面保证代码质量,但如果方向本身就是错的呢?下一节我们看 SDD——如何从宏观层面确保"做的东西是对的"。

下一节:SDD——先写规格的契约思维