---
title: 唯快不破的 TDD
---

本文将辨述关于 TDD 的观点与论战，给出我认为的作为编码任务管理的 TDD 如何为个人开发提升效率，为读者减少分辨、纠结于各类 TDD 论战上的时间浪费，尽快投入实践；最后，我会给出一个 TDD 的工作流，以及作为个人效率工具的 TDD 实践如何落地，从而达到及时反馈，不断进步的目的。

## 我眼中的 TDD

> TDD 究竟是什么？究竟有没有用？使用了 TDD 是否真的能自然驱动出设计来？TDD 是否能应对频繁变化的需求呢？为什么要先写显然不能通过的测试用例？各路大神论战不休，我究竟听谁的？如何形成自己对 TDD 的看法？

在这其中，TDD 有没有用，是最首当其冲的问题。其次，无论是否有过 TDD 实践，相信很多人也对 TDD 能否驱动出设计来这个问题有所思考与观点。第二个问题是很有意思的，答案也出人意料地简单。不过，先让我们来看看第一个问题，即 TDD 究竟有没有用。要回答这个问题，我们就必须界定一下，什么是 TDD。

### TDD 究竟是什么

我认为，**TDD 是一种用于提升个人开发效率的方法论。它通过避免编写无效代码、缩短反馈周期等方式减少浪费，在你已有需求、有了设计的情况下，帮助你快速、高效、高质量地完成既定编码任务**。它的理念总结起来，有三点：

* 快速反馈
* 减少浪费（花在编写无效代码上的浪费、各种纠结的浪费）
* 天下武功，唯快不破

总而言之，TDD 是一种方法论，就好比 GTD 是时间管理的方法论，PKM 是个人知识管理的方法论一样，TDD 是一种提高个人开发效率的方法论。简单来说，它帮你更快地完成代码，不仅如此，它还能促使你更高效、更高质量地完成。

因此，在讨论 TDD 时，我们所提的问题，都应该从 TDD 是否真的能有效提高效率的角度来回答。当你在问 TDD 究竟有没有用时，其实你是在问，TDD 是否真的能有效提高开发效率？相比其他需求/任务管理的方式，TDD 优秀在哪里？很关心这个问题的同学，请跳到 [TDD 工作流与实践方式]() 一节，我会告诉你，TDD 提高了哪些方面的效率，以及我们怎样来实践。在此之前，对于 TDD 仍有某种存在而挥之不去的疑惑的同学，请接着往下看，我会进一步定义 TDD 的边界，即哪些问题，你不能向 TDD 寻求。

### TDD 不是什么

我所见关于 TDD 的边界讨论中，最常见的三个东西便是：**需求**、**设计**、**代码质量**。

* TDD 不能帮你设计 - 如果你本来不会设计、没做设计
* TDD 不能帮你明确需求 - 如果你本来需求就不确定
* TDD 不能帮你提高代码质量 - 如果你本来就不知道怎么写整洁代码

>  你说的我都懂，但我就想知道 TDD 究竟能不能自然地驱动出设计来呢？

如果这里的“设计”做“特定适合于当前问题域的设计”解，那么回答是，不能。

我对 TDD 的定义中，讲到它是一种当你“有了设计”之后发挥作用的技术。也就是说，设计不仅是 TDD 之前需要做的工作，而且是需要设计知识来发挥作用的领域，它是在 TDD 的能力边界之外的。熊节3年前已经在 [虚拟座谈会](http://www.infoq.com/cn/articles/virtual-panel-tdd) 上谈到，要使用 TDD 的前提就是你要：

1. 会做设计
2. 做设计

尽管，TDD 的实践手段确实会使得驱动出来的代码具备一定的特征，比如输入输出明确、依赖更加独立等，但我认为这种特征并不是定向的，更不能认为其就是特定于当前问题域的恰当设计。想通了这个边界，事情就很自然了：如果你本来就不知道如何写出整洁代码，那么用了 TDD 显然也不能写出整洁代码（而是应该去看 [重构](https://book.douban.com/subject/4262627/) 或者 [编写可读代码的艺术](https://book.douban.com/subject/10797189/)）；如果你本来就不知道如何设计，那么用了 TDD 显然也不能“驱动”出设计（而是应该去看 [设计模式](https://book.douban.com/subject/1052241/)）。

## TDD 论战回顾

历史上发生过几次比较大的 TDD 论战，从可以看到的时间顺序大概是：

1. 2011年2月12日，CoolShell 博主陈皓首先提出，[TDD 并不是看上去的那么美](http://coolshell.cn/articles/3649.html) ，提出了关于测试范围、TDD 与设计方面的反对意见
2. 2011年2月23日，infoq 主编张凯峰随即组织了一场虚拟座谈会 [TDD 有多美？](http://www.infoq.com/cn/articles/virtual-panel-tdd) 继续论战，这里熊节提到了 TDD 并非毫无设计，而是相反
3. 同日，博客园一位博主 Todd Wei 撰文 [TDD 到底美不美](http://www.cnblogs.com/weidagang2046/archive/2011/02/23/1963277.html)，力挺 CoolShell，并提出 TDD 无法应对需求频繁变动场景的诘难
4. Martin Fowler, Kent Beck, DHH 三位大神的 [Is TDD Dead](https://www.youtube.com/watch?v=z9quxZsLcfo) 讨论

讨论是有益的，但是过多纠结在各方观点中无法决断、无法形成自己的观点，则是最大的浪费。我通读了这些论战，就其中观点与同事交流过，最后形成了自己的看法，希望这一小节，能让读者进一步明白我眼中的 TDD 是/不是什么，放下纠结，挽起袖子就是干。仝健老师说，**纠结才是最大的浪费**。我觉得很对。

在 [TDD 并不是看上去的那么美](http://coolshell.cn/articles/3649.html) 一文中，作者关于 TDD 本身的论点几乎全是错的，这恰好完美验证了他在 [这里](http://coolshell.cn/articles/3745.html) 所提到这篇文章的真正论点：最重要是人的问题。正因为 TDD 并没有看上去那么简单，需要很多动手实践和思考，所以才不是适用于所有人。毕竟，人是软件项目中最复杂的因素，也是倾向于抵抗力最小路径的，凭什么要求别人去学会一种如此复杂、需要耐心和方法坚持的方法论实践法？

这个观点我其实是同意的，不过它谈到的是从组织角度讲 TDD 推行所存在的问题。推行，这是另一个问题域，好比你会写代码和会教别人写代码是完全不同的事情一样。本文不会涉及 TDD 如何在组织推行的问题，也不会涉及 TDD 的流程价值，而只谈，作为个人开发效率提升工具的 TDD。如果你觉得，你想要更高效更高质量地写代码，这篇文章可能有帮助。

然后，熊节在 [TDD 有多美？](http://www.infoq.com/cn/articles/virtual-panel-tdd) 则提出，TDD 作为一种设计方法，本来就要求你会设计做设计。首先厘清了 TDD 与设计的关系问题：TDD 促进你思考、表达你已有的设计，但不能替代它。如果你本来就不会设计，那你该先学会设计；如果你会但你不做，那谁也帮不了你，顶多就是促使你思考一下。

那么同理，代码质量与设计也是一样的道理：TDD 的工作方式（重构环节）促使你写出更整洁的代码，但如果你不会、不做，那也别赖 TDD，换谁都没办法。

接着是 Todd Wei 的这篇文章：[TDD 到底美不美](http://www.cnblogs.com/weidagang2046/archive/2011/02/23/1963277.html)，它提出了一个更难回答的问题，即 **TDD 与需求的关系 **问题：TDD 适用于需求非常固定的场景，然而不适用于需求经常变动的场景，毕竟需求变了就要改测试，测试扔了不就是所有成果都白费了么。

这个问题我也纠结了很久，于是我发了一封邮件请教了一下仝健和小波等老师。测试作为需求的等价表述，需求变，测试肯定要变，这个不管你是 TDD、先写测试后写测试都一样——除非你不写测试。那么上面的诘难其实是，需求变了怎么办？**改呗**。那需求反正肯定是要变的，这是软件工程界的共识，不管你用什么开发方法都一样。那我能做的，就是改起来也快。如何做到呢？那可不就是 TDD 的问题域了嘛。

“我做得比你快，我改得也比你快。”改起来越快，成本就越低，扔起来就越不心疼，你做实现的限制就越小。这正是我们提倡已久的理念：**天下武功，唯快不破**。

## TDD 工作流与实践方式

终于进入工作流与实践法了，本节讲述：怎样更快。

### 任务分解 Tasking

任务分解是 TDD 工作流的第一步，也是重中之重。它的目标，是对已有需求做一个完全穷尽、独立的分解，产出一个任务列表。这个列表不需要是粒度很细的测试用例（也不推荐是），只需要从逻辑上完全覆盖业务上的需求即可。以 [FizzBuzzWhizz](https://raw.githubusercontent.com/htoooth/FizzBuzzWhizz/master/problem.md) 这个问题为例，结合 [JUnit 5](http://junit.org/junit5/docs/current/user-guide/) 的`@Nested`注解，我甚至可以直接把我的 tasking 转化成代码，达到 todolist as code 的效果，从而避免了维护一个额外的任务列表的重复：

![junit-5-tasking-todolist-as-code](http://7xqu8w.com1.z0.glb.clouddn.com/tdd-junit5-tasking-todolist-as-code.png)

为什么一个不多不少描述原有需求的 tasking 如此重要呢？它是你减少浪费的基石：凡是不在需求中的任务，我们不会去 task。那么你可能会问，我就是想要设计得尽可能灵活以应对未来可能的需求变化啊。如果你听说过 YAGNI(You Ain't Gonna Need It) 原则，那么你就会明白了：整个 TDD 方法论都是建立在变化一定会发生这个基础上的。那些仅仅是“可能”会发生的事情，我们不会去做，仅凭这个就可以节省大量精力了。那么需求真的变了怎么办？改呗。我们从来没有保证过 TDD 会让应对变化变得容易，但它会让你有底气去做出修改。还是那句话，**天下武功，唯快不破**，改得越快，成本越低。

在这个例子中，仝健老师说，我的代码中都抽取出了“特殊数”这个概念来，但需求中有强调特殊数需要是可配置的吗？没有的话，这样的需求就是多余，这样写出来的代码就是浪费。我录制了我 TDD 时的视频，那段代码的编写时间，加上我重构快捷键的不熟悉等，一共花去我14分钟。想想，平时折腾这种其实根本无效的需求，我们浪费了多少时间。

### 可验证的用例 Verifiable ACs 

一个任务（task），可能可以产生多个测试用例。上一步中，我们有了一个与需求等价的任务列表以后，这一步的任务是把这些任务列表进一步分解成可验证的测试用例：也就是一个个 input->output 明确的测试用例。有了这些测试用例，我们就可以开始 TDD 中经典的红绿循环了。上面两步，是开始写测试前的重要环节，搭起了高层的需求到细节的测试用例之间的桥梁。

比如，上面的第一个 tasks，我可以进一步细分出这些可验证的测试用例来：



![tdd-from-tasks-to-verifiable-acs](http://7xqu8w.com1.z0.glb.clouddn.com/tdd-from-tasks-to-verifiable-acs.png)

### 红绿循环 Red-Green-Refactor

![red-green-refactor](http://7xqu8w.com1.z0.glb.clouddn.com/tdd.png)

这是众所周知的 TDD 红绿循环图。拿到上面分解得到的可验证的测试用例后，你就可以开始写代码了。这一步要注意的点，有两点。

* 最小实现（同样，出于减少浪费的精益思想）
* 出现了坏味道要重构（TDD 实践中促进代码质量的一步）

什么是坏味道，以何手法改进之，不是 TDD 的问题域，请咨询 [重构](https://book.douban.com/subject/4262627/) 一书。

用代码来描述 TDD 的整个工作流，其实就是：

```javascript
function TDD(requirements, designs) {
  let tasks = tasking(requirements, designs)
  let testCases = tasks.map(task => verifyableACs(task))
  let code = testCases.forEach(redGreenRefactor()).collect()
  
  return code
}
```

### 持续修正 PDCA: Plan-Do-Check-Action

这是著名的戴明环，并且这个实践主要取自仝健老师的 [编程的精进之法](https://www.zybuluo.com/jtong/note/504192) 一文，详细的论述读者可移步。这里会讲，如何把这个训练-反馈的实践用到 TDD 中来。

首先，我们上面的第一二步（任务列表分解 与 可验证的测试用例）显然都是在做plan；而第三步则是 do 的过程。真正为你的实践起到反馈、修正作用的，是 check 和 action 的环节。

#### Check：定量化的反馈

check，检查，回顾。回顾什么内容呢？简单来说，就是对你前面 TDD 实践过程质量的检查，仝健老师提到，可以从两个方面来 check：

* 对比实际完成的 tasks 总量（任务列表）
* 对比实际完成 tasks 的时间（时间估计）

实际检查下来，如果与你的期望偏差较大，则无非可能是时间估计不准，或是任务列表扩张了。通过进一步的检查，你可能发现大部分的问题出在自己的身上，比如：

* 任务列表扩张了：有可能是自己任务分解时，对实际步骤就没有想得很清楚，它可能反映出你在或业务或技术或流程上的不熟练；也有可能是你分解的任务看起来穷尽了，但其实并没有穷尽，导致做的时候又发现很多事情需要处理
* 时间估计不准：可能是自己技术不熟、工作效率还不够高，比如 IDE 快捷键不熟、编辑器不熟、MAC 快捷键不熟、没有总结模板操作、获取信息方式不高效、手速太慢等。任务列表的扩张同样可能导致时间估计不准

如何来获得这些检查时所需要的材料呢？我可以推荐两个方法：

* 视频录制复盘
* 定量化的时间分析

#### Action：持续提高

有了上一步的检查数据，action 就很好做了，其实就是错哪改哪，缺啥补啥，针对特定的问题，制定出改进计划、可验收的标准，并通过刻意练习来提高。比如，发现快捷键不熟悉，那么 action 可以是学习快捷键表，验收标准是完成练习时不需要动用鼠标；如果是手速较慢原因，那么 action 可以是在 [typing.io](http://typing.io) 等专门练习代码手速的网站上练习手速，验收标准是达到 每分钟45的 WPM；等等。

## 总结

本文主要讲了三个方面的内容，循序渐进：

1. 首先我们一句话介绍了 TDD 是什么。TDD 是一套提升个人开发效率的方法论和实践，其理念在于**减少浪费**。它不是其他的什么东西
2. 然后，我们简要回顾了 TDD 历史上的论战。这一节我们理清了 TDD 即非需求管理工具，也非代码质量/设计工具，指出相关问题应学习相应领域知识来解决。本节目的是进一步定义 TDD 的边界，避免在形上的论战中浪费时间精力。毕竟，**TDD 是一门实践的艺术，而纠结则是最大的浪费**
3. 最后一章从顶向下介绍了 TDD 的工作流和实践流程。其背后理念也是减少浪费，**唯快不破，无效代码一行都不多写**。
