<!-- title: 重构一跑全红：你的TDD走歪了 -->
<!-- domain: programming -->
<!-- video: https://www.youtube.com/watch?v=EZ05e7EMOLU -->

# 重构一跑全红：你的TDD走歪了

## 导语

你大概率经历过这个场景：代码的行为一点没变，只是重命名了一个方法、抽了一个函数、调整了一下内部结构，跑一遍测试，满屏飘红。团队复盘，最后往往得出一个结论——“这代码动不得，重构成本太高”。于是测试从“变更的安全网”，悄悄变成了“变更的手铐”。

十年前，英国资深工程师Ian Cooper在NDC伦敦大会上做了一场约54分钟的演讲《TDD, Where Did It All Go Wrong》（TDD到底是在哪一步走歪的），正面回答了这个让无数团队破防的问题。他的核心判断很扎心：**不是TDD失效了，而是我们把“单元测试”的“单元”测错了对象**——Kent Beck当年说的unit是“行为单元”，后来被我们偷偷换成了“一个类”。这场演讲至今仍是测试主题下被引用最多的长视频之一，无论你写Python、Go还是做网络自动化脚本，它都能帮你把测试这件事的底层逻辑理顺。

## 视频背景卡片

| 项目 | 内容 |
|------|------|
| 视频标题 | TDD, Where Did It All Go Wrong |
| 频道 | NDC Conferences |
| 时长 | 约54分钟 |
| 播放量 | 近百万级，测试主题常青经典 |
| 核心主题 | TDD的初衷、走歪的过程与回归路径 |
| 演讲者 | Ian Cooper，英国资深工程师，开源消息框架 Brighter 作者 |

## 核心内容拆解

### 一、开场之问：安全网怎么变成了手铐

Cooper开场的姿态不是布道，而是“验伤”。他描述了一个几乎所有团队都遇到过的困境：TDD承诺的图景是“放心大胆地改代码，测试兜底”；可现实是，真正需要测试兜底的时刻——重构——恰恰是测试最先报警的时刻。行为没变，测试红了；你修的不是产品代码，而是几百个测试用例。（此处为演讲内容的大意转述。）

我的延伸分析：这个开场之所以击中人，是因为它指出了一种**责任倒置**。TDD的本意是“测试因业务行为失败，所以你改代码”；走歪之后变成“测试因内部结构变化失败，所以你不敢改代码”。前者测试在保护业务，后者测试在保护代码的形状。一旦团队长期处于后者，对“写测试”这件事的信任会整体崩塌——很多人由此得出“TDD没用”的结论，其实他们从没做过真正的TDD。

### 二、回到源头：Kent Beck的本意是用例子驱动设计

演讲用了相当篇幅做“考古”。TDD并非2000年代的新发明，它源自Smalltalk时代的开发传统，由Kent Beck整理复兴，最终沉淀在2002年的经典著作《Test-Driven Development: By Example》里。Beck的本意是：**先写一个表达行为的失败测试，再写恰好让测试通过的代码，然后重构**——这就是红-绿-重构循环。测试在这里首先是“设计与规格工具”，其次才是回归防线。

关键在“unit”这个词。Beck语境里的unit test，测的是**unit of behavior（行为单元）**——一个用户可感知的功能切片。Cooper特别强调：行为单元的粒度可以很大，大到“用户提交订单”这个层面，完全不必落到每个类每个方法。（大意转述。）

我的延伸：这一点很多中文资料从未讲透。“单元”不是解剖学意义上的最小零件，而是**需求意义上的最小承诺**。理解了这一层，后面所有的争论才有裁判标准。

### 三、走歪的分水岭：“单元”缩水成了“一个类”

那到底是在哪一步歪的？Cooper给出的答案是2000年代中期测试框架与mock框架（Moq、Mockito、Rhino Mocks这一批）大爆发之后：社区把“unit test”的unit**默认理解成了一个类**，再配上mock，把被测类的一切协作者都替掉，美其名曰“隔离”。

他把两大流派摆上台面：经典派（Detroit风格，真实对象+状态验证）与伦敦派（London风格，mock协作者+交互验证）。在Feathers著名的定义里（出自2004年《修改代码的艺术》），一个测试只要碰了网络、数据库、文件系统，就算integration test而非unit test——这条定义被普遍误读成了“必须mock掉一切外部协作”。（大意转述。）

我的延伸：这里要替伦敦派说句公道话——伦敦派自己的成熟形态（Freeman与Pryce的《测试驱动的面向对象软件开发》）主张的是**由外向内分层驱动设计**，并非“每个类都配上满堂mock”。Cooper批判的是mock被滥用后的民间形态，不是流派本身。这个nuance视频里点到即止，读者自行延伸阅读时值得留意。

### 四、代价清单：结构耦合的四类假信号

测试焊死实现细节之后，Cooper列出的账单相当难看，我归纳为四类：

**假警报**：重构（行为不变的结构调整）必然导致测试变红。红-绿-重构的第三步“重构”直接瘫痪，只剩红和绿，代码质量迭代通道被堵死。

**过指定**：mock验证的是“你有没有按我预期的顺序、以我预期的参数调用了协作者”，这是在验证实现路径而非结果。换一种同样正确的实现，测试照样红。

**慢与脆**：内部每个协作者都要搭建替身，Arrange部分越写越长；一个内部改名，几十个测试连锁报错。

**信任破产**：当维护测试的成本超过收益，团队开始删测试、跳过测试、“走过场式”地写测试。最后不是TDD死了，是测试文化死了。

我的延伸：这里面最隐蔽的是**测试债务比代码债务更难被发现**。代码坏了会报错，测试“坏了”（测错了对象）不会报错——它只是让你的每一次正确改动都显得像事故。

### 五、一条核心判据：测行为，不测结构

整场演讲最重要的产出，是一个可以立刻上手的判据：**行为不变而测试变红，错的是测试，不是代码。**

Cooper给出的自查方式很朴素：问自己“这个测试的失败，能不能被业务方理解？”如果测试失败的原因是“方法被改名了”“内部调用顺序变了”，业务方只会一脸茫然——那你测的就是结构。（大意转述。）反过来，好的测试失败时，理由应该是一句业务语言：“下单没有扣库存”“账单金额算错了”。

我的延伸：这条判据其实给了“该改测试还是该改代码”一个清晰裁决。很多团队在红灯面前的争论，本质是缺这条判据：行为错了→改代码；行为没变而测试错了→改测试，且要追问当初为什么写出这种测试。两件事混在一起，红灯就失去了信息量。

### 六、处方一：把测试抬到公共端口

怎么测才不焊死结构？Cooper的答案是**沿模块的公共API测**——把“单元”重新定义为行为单元，测试从模块对外的端口打进去，把它当成一个黑盒：给它输入，断言输出与副作用，不关心内部怎么实现。

支撑这一做法的架构抓手是六边形架构（Hexagonal Architecture，Cockburn提出）：把业务逻辑放在核心，通过端口与适配器和UI、数据库、消息队列打交道。这样一来，业务逻辑可以在“无界面、无数据库”的头里跑测试——不慢、不脆，而且天然只暴露行为。（大意转述。）

我的延伸：这不是让你放弃分层测试，而是**调整默认档位**。外圈放少量故事级测试（story test）守住业务承诺，内圈的测试只在确实需要时才写。多数团队的问题恰恰是倒置的：内圈几百个类级测试密不透风，外圈几乎没有。

### 七、处方二：替身纪律——只在边界mock

关于mock，Cooper的纪律可以浓缩成两句话：**只mock边界上的东西；优先验证状态而不是验证调用。**

边界是什么？你的代码与“你不拥有的世界”交界处：网络、数据库、消息总线、文件系统、时钟这类慢的、不确定的、测试环境里不可控的东西——在端口处用替身换掉它们。而模块内部的协作对象，尽量用真实对象，跑完了断言最终状态，而不是检查“你有没有调用过它三次”。他还引用了一个流传很广的原则：**别mock你不拥有的类型**，第三方库的类不该成为你替身体系的地基。（大意转述。）

我的延伸：这条纪律的价值在于把“隔离”这个词的意思修正了。隔离的目的从来不是“把类关进真空罩”，而是**把不确定性关在门外**。真实对象并不可怕，不可控、慢、会漂移的东西才可怕。想通这一点，mock的使用量通常会降一个数量级，测试的可信度反而上升。

## 知识结构图

```mermaid
flowchart TD
    A[TDD的岔路口] --> B[原意：行为驱动]
    A --> C[走歪：结构驱动]
    B --> D[unit即行为单元]
    B --> E[红绿重构循环]
    B --> F[测试是安全网]
    E --> G[重构须保持全绿]
    C --> H[unit缩成单个类]
    C --> I[内部协作全mock]
    H --> J[测试焊死实现]
    I --> J
    J --> K[重构就亮红灯]
    J --> L[测试沦为负债]
    K --> M[没人敢动代码]
    L --> N[删测试走过场]
    M --> O[代码逐渐腐烂]
    N --> O
    A --> P[回归正道]
    P --> Q[沿公共端口测]
    P --> R[替身只留边界]
    P --> S[六边形架构托底]
    Q --> T[重构自由]
    R --> T
    S --> T
```

## 实战启示

看完这场演讲，建议你本周就对自己的测试套件做一次“三问体检”：

**第一问：我敢不敢重构？** 挑一段想优化的代码，只做结构调整、不改行为，跑测试。如果红了，别急着修——先判断红灯归属：行为真的变了吗？没有，那就是测试在测结构，把这些测试视为债务登记下来。

**第二问：我的测试失败时，说的是业务话还是代码话？** 把每个失败信息读一遍。如果失败原因是“某方法不存在”“某mock未被调用”，它就是过指定测试；如果失败原因是“金额算错”“状态没流转”，它才是行为测试。

**第三问：我的mock有没有越界？** 数一数替身的对象：如果大量替身是自家模块的内部类，替身纪律已经失守；替身应该集中在端口上——数据库、网络、消息总线、时钟。

对通信行业的读者多说一句：做核心网自动化、信令仿真脚本时，这套思想同样直接适用——把网元接口、SBI调用、时钟当成端口和替身，把签约判定、话单校验这类业务逻辑写成不碰网络的“函数式核心”，测试速度和稳定性都会上一个台阶。这个迁移是我基于演讲原则的延伸，不是视频原内容。

一句话概括行动指南：**下一个测试，从模块的公共API开始写，从最像业务承诺的那条路径开始写。**

## 延伸学习路径

1. **Kent Beck《Test-Driven Development: By Example》（2002）** —— TDD的“原教旨”文本，看懂unit的本义，全书用两个小项目演示红-绿-重构。
2. **Michael Feathers《Working Effectively with Legacy Code》（2004）** —— “什么才算unit test”的经典定义出处，也是处理无测试遗留代码的百科全书。
3. **Martin Fowler《Mocks Aren't Stubs》** —— 讲清Test Double家族（stub/mock/spy/fake）与两大学派分歧的必读文章。
4. **Steve Freeman & Nat Pryce《Growing Object-Oriented Software Guided by Tests》（2009）** —— 伦敦派的成熟正名之作，读完你会明白mock本不该被滥用。
5. **DHH《Test-Induced Design Damage》（GORUCO 2014演讲）** —— 有趣的反方视角：Rails作者批评为可测性牺牲设计，看完正反双方你对TDD的判断会更立体。

## 一句话总结

测试是行为的安全网，不是结构的封条——对象测错了，再自律的TDD也会把团队逼上删测试的不归路。

## 参考来源

1. [TDD, Where Did It All Go Wrong — Ian Cooper, NDC London 2016](https://www.youtube.com/watch?v=EZ05e7EMOLU) — 本文分析的核心视频
2. [Mocks Aren't Stubs — Martin Fowler](https://martinfowler.com/articles/mocksArentStubs.html) — 测试替身与两大学派分歧的经典阐述
3. Kent Beck, *Test-Driven Development: By Example*, Addison-Wesley, 2002 — TDD原始理念
4. Michael Feathers, *Working Effectively with Legacy Code*, Prentice Hall, 2004 — unit test定义与遗留代码处理
5. Steve Freeman & Nat Pryce, *Growing Object-Oriented Software Guided by Tests*, Addison-Wesley, 2009 — 伦敦派成熟方法论

---
> 本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵**爱卫生**（微信ID: gprshome201101）维护，聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。
> 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
