什么是测试驱动开发(TDD)

“只有不断地写代码来修复一次失败的测试。”这就是测试驱动开发,或简称为TDD。我们先编
写一个测试,然后写代码来使这个测试通过。我们会发现我们所能做的最好设计依赖于现有
的能让我们不破坏事情的测试。这个用于构建软件的方法鼓励好的设计、产出可测试的代码
并且使我们免于由于有缺陷的假设而过度工程化我们的系统。所有这些是由可执行测试的方
法来驱动我们设计的每一步这一简单动作来完成的,这样能够推动我们达到最终的实现。

做为一种不仅仅验证软件的正确性的应用测试方式,严格地说TDD不是一个新发明。之后再说
,很多老前辈讲述他们怎么习惯于在写代码前编写测试的故事。现在这种开发软件的方式有
了名字叫做TDD。这本书主要致力于测试驱动开发是什么、怎样将测试驱动开发应用于开发软
件过程中的不同目标。

根据主流认识,TDD仍然是新的。正如现在的日用品是以前的奢侈品一样,开发和设计技术通
常是由几个做为奢侈的有经验的从业人员开始然后经过几年当先行者已经证实和发展了这项
技术后而被大众接受。

我们将开始列出用当前状态软件开发惯例来交付软件所带来的挑战。一旦我们把我们将要实
现什么和有什么事阻碍我们这两个问题放一起,我们可以建立一个路标来探索TDD是怎么样工
作的,并且认同TDD能够帮助我们解决那些问题,而且我们将着眼于这些我们在成为熟练人员
的“旅程”中使用的工具。

1.1挑战:正确地处理正确的问题

软件开发的功能是为支持一个组织的操作和业务。我们的焦点是由专业软件开发者交付那些
帮助我们的组织提高效率和生产量及降低操作成本等的系统。

回顾我做专业软件开发者的那些日子和经历有文档证明的已印刷好的文章的那十年就像文人
对世界战争故事的证明一样,我们只能推断大多数组织能够在以支持他们的业务为目标时做
得更好。简单地说,我们正在构建不完全正确工作的系统;甚至它们能够无故障地工作,试
图解决错误的问题。基本上,我们正在写那些不能确切满足需求的代码。

下面,让我们看一下怎样应对产生不好代码和错过客户那像活动靶子一样的实际需求这样几
部分的挑战,然后交付给用户正确有用的解决方案。

1.1.1产生不好的代码

当软件产业前进的几十年过后,软件产品的质量仍然是一个问题。考虑最近几年焦点由时间
到市场,开发软件总量的增长和对新技术吸收的趋势,软件开发组织持续面对质量问题是不
奇怪的。

质量问题有两个方面:高缺陷率和缺乏可维护性。

用缺陷打了许多洞

缺陷通过使系统不稳定、不可预知或潜在的彻底不稳定而消耗了多余的成本。它们降低了我
们交付软件的价值,有时会达到制造比价值更大的损失的程度。

我们除去缺陷的办法是通过测试,如果我们看到软件正常工作,那么我们试着从不同角度破
坏它。

测试已经被当做一个关键成分在软件开发中被建立起来,但传统软件测试的方法—在代码冻结
后一个冗长的测试阶段—为进步留下了很大的空间。例如,修复由测试发现的缺陷的成本特别
巨大,甚至比我们在缺陷被引入代码库时发现它们要高两倍多。有缺陷意味着我们不能交付
。发现和修复缺陷使交付越慢且成本提的更高,我们的能力就变得越少。
缺陷也许是差的代码最明显的问题,但这样的代码也是未来软件维护、交付慢和高
成本开发的一个梦魇。

维持和减慢开发的梦魇

写得好的代码展示了好的设计和一个所有资源不重复责任平衡分配。差的代码在很多方面不
能与之很好地合作则是梦魇。其中一个方面是代码难以读懂,因而,难以改变。就像一次速
度不够的碰撞,改变有问题的代码会破坏系统中其它部分的功能性,而且以缺陷为形式的破
坏副本也被期望已经解决了。列表继续。

“我不想碰那个。让它永远那样吧,我不知道我动了之后会破坏什么。”
这是一个非常真实的问题因为软件需要改变。比每次重写代码更好的办法是我们需要改变现
有代码或者增加新的代码,需要能在已有代码的基础上进行编译。那就是所有维护要做的,
也是使我们能够满足业务变更的需要。不可维护的代码降低了我们预期的进行速度,导致给
发布带来了持续增长的压力,也使我们发布更差的代码。如果我们想持续地发布产品,这种
恶性循环必须停止。

好像这些问题不足,仍有麻烦不能满足实际需求。让我们讨论一下。

1.1.2不能满足实际需求

没人喜欢瞎买东西。但软件开发组的客户们已经多次被迫这样做了。以规格说明书交换,软
件开发者已经开始编译规格说明书所描述的—仅是为了在十二个月以后发现规格说明与客户需
求没有确实匹配。不提那个,特别地在现代狂热的世界商务,客户当前的需求与他们去年的
需要有着非常令人瞩目的区别。

做为这种重复的一个结果是不能交付客户所需要的,我们做为一个行业已经设计出运行软件
项目的新方法。我们曾试着工作得更努力、时间更长一些来建立规格说明书,但这让事情变
得更糟,为一个已交付的系统留下更多的时间来进行改变。而且,在早期盯住更多的细节是
与用卡片建一幢房子是有联系的。设想是建立在假设基础上的规格说明书,其错误能够很容
易地打倒整个项目。

我们行业的跟踪记录倾向于令人沮丧的阅读。不管怎样我们不需要陷入整个沮丧中,因为知
道了如何解决这些问题。敏捷软件开发包括极端编码和并列开发的方法,表现了我所知道的
最有效的修正方法。这本书的其余部分会带给我们一个对敏捷开发关键组成部分的彻底理解
—那就是测试驱动。

1.2解决方案:测试驱动

就像我们正面对的问题的两部分—差的代码和未满足确切需求的代码—我们后面章节将要介绍
的解决方案也是分为两个方面。一方面,我们需要学习怎么正确做事。另一方面,我们需要
学习怎么做正确的事。这本书我描述的解决方案—测试驱动—很大程度上与这两方面相同。这
个解决方案的两个方面的微小区别是我们怎样利用测试帮助我们建立可维护的、可工作的和
满足了客户确实的当前需求的软件。

在一个较低的级别,我们把测试驱动代码的技术叫TDD。在一个较高的级别—从产品特性和功
能上看—测试驱动系统使用一种类似的技术叫做“接受TDD”。图1.1描述了从内部和外部两个方
面改善的这种联合。

正如我们可以从图1.1看到的,在这两个明显的层上我们测试驱动产品的包括内部质量和外部
质量或可感知的质量的整个改善。在下一节中,我们将会发现TDD和“接受TDD”是怎样完成这
些改善的。在我们深挖这项技术前,让我们先集中精力在这些技术怎么帮我们解决发布中遇
到的挑战。

1.2.1 TDD带来的高品质

TDD是一种帮助我们避免程序错误、鼓励好的设计和规范过程的开发方法。TDD
通过让我们写一些小的、自动的测试,这些测试最终建立了一个非常有效的预警系统来保护
我们的代码免于衰退。不能在事后把质量加到软件中,TDD改善短的开发周期很好地与从最初
编写高质量的代码结合起来。

短周期是与我们过去常用的开发方法不同。通常我们先设计,然后实现设计,接着测试实现
,不知何故,通常不够彻底。(毕竟我们是好的程序员,不会犯错,对吗?)TDD转变了这种
思想并提出我们应该先编写测试然后再编写代码来达到这个清晰的目标。设计是我们最后做
的事情。看我们已有的代码来发现可能的最简单的测试。

在这个循环中的最后一步叫重构。重构是一个用一定规则的方法把代码从一种形式或结构转
成另一种,删除重复,并逐步把代码向我们能够想象的最好的设计方向移动。经常地重构,
我们能够培养我们的代码库并且逐渐地发展我们的设计。

如果你对我们所讨论的TDD不是十分清楚的话也不必担心。我们会在1.3节对这个循环进行更
进一步的了解。

为了翻新迄今为止我们所学的TDD,它是一种帮助我们在每个阶段使用(尽可能)最好的设计
编写彻底测试过的代码的开发技术。TDD仅仅帮我们避免了差的代码的恶性循环。测试驱动是
所有解决方案中最好的一个。

说到质量,让我们说一点更抽象的概念以及它们的意义。

不同形式的质量

显然,通过当今世界合作的质量保证部门,人们要把质量和使用软件后发现的缺陷数量联系
起来。有些人认为质量就是软件达到使用者的需要和期望的程度。有些人认为不仅是外表上
可见的质量而且软件内部的质量也应该被考虑(外部质量指像开发、维护等成本)。TDD用它
的设计导向和面向质量本质在所有这些方面为改善质量而做出了贡献。

一个缺陷滑向产品的原因很可能是因为没有测试验证我们代码中的特殊执行路径,而它确实
会被执行。(另外一个候选的原因是我们的懒惰:没有执行所有的测试或测试执行得有一点
过分,从而留下了缺陷。)

TDD通过确保系统中没有不必要的代码来修正这种情况—因而由测试来执行。通过扩展测试覆
盖和让所有测试都自动化,TDD有效地保证了你为工作所写的所有测试工作,并且保证质量是
我们如何成功跟上正确的测试用例的一个功能。

那个目标的一个有效部分是测试技巧—我们从普通用例中得到测试用例的能力,边缘的用例,
可预见的用户错误,等等。TDD可以在让我们聚焦于模块的公共接口、类和你所有的这些给予
帮助。在不知道实现是什么样子的时候,我们最好定位于设计出这个盒子并且聚焦于代码应
该怎样行为表现,开发者的客户端代码怎样故意地或错误地使用。

TDD对代码和设计的质量的关注已经非常有意义地影响到我们宝贵的开发时间,有多少用在修
复缺陷胜于实现新功能或改善现有代码库的设计。

较少的时间用于修复缺陷

TDD通过减少修复缺陷来帮助我们加快速度。在一个缺陷被引入到系统两个月以后才修复它所
用的时间和金钱远比在它被引入的那天修复要多,这是常识。首先无论怎样我们能做的是减
少缺陷引入的数量而且帮助我们在那些缺陷一进入时就被我们找到,是必须要达到的。

在小的步骤中先进行测试确保了我们几乎从不需要接触调试器。我们精确地知道我们加的几
条线会中断测试并且能够立即找到问题的源头,避免那些我们经常在程序员战争的故事里面
听到的长的正在调试的对话。我们能够很快修复我们的缺陷,减少项目的业务成本。每一个
错过的缺陷会耗费几百到几千美元。不必花费时间来发现调试器,让它允许在其它有用的活
动上花费更多的时间。

事实上我们正更快地交付着所需要的功能意味我们有更多的可用时间来清理我们的代码库、
达到最新开发工具和技术的相同速度、赶上我们的同事等等—更多的可用时间来改善质量、信
心和速度。这些是我们要反馈给我们有效地进行测试驱动的能力的所有事情。这是一个良性
循环,一旦进入这个循环,就开始了不停地改善!

我们很快将要讨论采用和实行TDD的更多的好处—对于你我这样的程序员的好处—但在开始之前
,让我们先讨论一下我们的解决方案中上述的第二个对于交付的挑战:“接受TDD”。

1.2.2用“接受TDD”满足需求

TDD帮助我们用高技术质量来建立代码—这些代码会做我们想要它做的事情,并且这些代码容
易被读懂和发挥作用。无论如何,我们用TDD开发的代码使用逻辑隔离块测试胜于用特性和系
统能力测试。此外,即使写得最好的优先测试的代码也会实现错误的事情、客户确实不需要
的东西。这里是“接受”测试驱动开发引起人们注意的地方。传统的为系统添加特性首先要做
的事情是编写一种需求文档,接下来是实现,让开发团队测试这个特性,然后让客户接收测
试这个特性。如1.2节所示,“接受TDD”与传统方法之间的不同在于“接受TDD”在实现之前移动
了测试功能。换句话说,我们把需求转换成一系列的执行测试集然后按照测试实现胜于按照
开发者口头说明来实现。

“接受TDD”为我们交付一个好的产品通过在程序员和客户间架设桥梁提供了缺失的成分。胜过
清理武断的需求文档,在“接受TDD”中,我们争取关闭协作和定义清楚的、没有歧义的测试来
明确地告诉当我们说一个特性完成时意味着什么。