2.1 TDD:先写测试的工匠精神
老王上周刚经历了一次"回归灾难":他修了一个密码验证的 Bug,但三天后发现注册功能挂了——因为修 Bug 时不小心改了共享逻辑。
"如果我有测试,这个回归 Bug 根本不会发生。"——这就是老王坚持 TDD 的原因。
❌ 不用 TDD vs ✅ 用 TDD
❌ 不用 TDD 的开发方式
写代码 → 运行 → 手动测试 → 发现 Bug → 修 Bug → 又引入新 Bug → 继续修...- 手动测试不全面,每次改代码都可能破坏之前的功能
- 修 Bug 时容易引入回归(修了 A 坏了 B)
- 不敢重构,因为不知道会改坏什么
✅ TDD 的开发方式
写测试(失败)→ 写代码(通过)→ 重构 → 写下一个测试 → 循环- 每个 Bug 只被发现一次(变成测试用例)
- 重构有安全网(测试通过 = 没改坏)
- 测试即文档(看测试就知道代码应该怎么用)
TDD 的核心:Red-Green-Refactor
这不是三个步骤,而是一个永远不停的循环:
证明测试确实在验证某些东西。
先让它工作,不管丑不丑。
有测试保护,放心重构。
每个循环只要几分钟。一天下来,你可能跑几十个循环。
老王的实际操作
让我们看老王实现"字符串大写"功能的完整 TDD 过程:
第一步:Red——写失败测试
// 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——写最少的代码
// string-utils.ts
export function capitalize(str: string): string {
return 'Hello' // 硬编码——最简单的让测试通过的方式
}运行测试 → 通过! 但这显然不对。
第三步:加新测试,再次 Red
it('should work with any word', () => {
expect(capitalize('world')).toBe('World') // ← 硬编码会失败
})运行测试 → 失败! 迫使你写真正的实现:
export function capitalize(str: string): string {
if (str.length === 0) return ''
return str.charAt(0).toUpperCase() + str.slice(1)
}运行测试 → 全部通过!
第四步:Refactor——优化
测试通过后,老王安心重构,因为任何错误都会被测试捕获。
为什么 TDD 有效?
这不是玄学——有硬数据支持:
来源:Nagappan et al. 2008,Empirical SE 期刊,Microsoft/IBM 四团队研究
但长期维护成本显著降低
每个测试的标准结构
JUnit、pytest、Jest、Vitest……
TDD 的暗面
TDD 不是万能的。老王也承认有些场景 TDD 不划算:
| 场景 | 为什么不适合 |
|---|---|
| 探索性原型 | 需求还不清楚,测什么? |
| 快速变化的需求 | 测试改了又改,成本太高 |
| UI/视觉测试 | 界面布局用单元测试很难覆盖 |
| 一次性脚本 | 不值得投入 |
还有三个常见的反模式要避免:
- 过度 Mock——Mock 了所有依赖,测试通过了但实际运行还是挂
- 测试巨人——一个测试里 15 个断言,出错了不知道哪一步失败
- 说谎者测试——测试看着通过了,其实什么都没验证(异步代码常见)
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 有哪三个常见的反模式?分别是什么问题?
查看答案
- 过度 Mock:Mock 了所有依赖,测试通过了但实际运行还是挂
- 测试巨人:一个测试里 15+ 个断言,出错了不知道哪一步失败
- 说谎者测试:测试看着通过了,其实什么都没验证(异步代码常见)
下一节预告
TDD 从微观层面保证代码质量,但如果方向本身就是错的呢?下一节我们看 SDD——如何从宏观层面确保"做的东西是对的"。