测试驱动开发的艺术(txt+pdf+epub+mobi电子书下载)


发布时间:2020-05-16 18:42:16

点击下载

作者:(芬)Lasse Koskela

出版社:人民邮电出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

测试驱动开发的艺术

测试驱动开发的艺术试读:

版权信息书名:测试驱动开发的艺术作者:(芬)Lasse Koskela排版:JINAN ENPUTDATA出版社:人民邮电出版社出版时间:2010-11-01ISBN:9787115238368本书由人民邮电出版社授权北京当当科文电子商务有限公司制作与发行。— · 版权所有 侵权必究 · —第一部分TDD入门

这一部分是TDD(Test-Driven Development)入门,引领读者体验测试驱动的艺术。第1章来认识TDD和它的延伸概念——验收测试驱动开发(Acceptance TDD,ATDD),我们从最基本的知识入手,对这两种技术先有一个大概的了解。第2章进入实际动手环节,将通过修改和运行实际代码深入体验测试先行的好处。第3章将更进一步,向读者展示大规模重构的例子,借以看看设计将会有多么显著的变化。

几年来,通过向一批批的程序员讲授TDD,我体会到实践是最好的老师。在学习了第2章和第3章并亲手实现功能完善的模板引擎之后,就可以接触一些重量级的技术了。第4章介绍了运用TDD思想的一些技巧和方法,包括如何选择下一个测试并使其通过。此外,必要时还将讨论相关的设计原则和测试工具。第1章综述

我能忍受暴力,但施暴的理由却让我无法容忍。——奥斯卡·王尔德

测试驱动开发(Test-Driven Development,TDD),用一句话讲,就是“写代码只为修复失败了的测试”。我们先写一个测试,然后再写代码让测试通过。当我们在当前结构中找出最佳设计时,由于有足够的测试做保障,我们可以放心地改动现有设计而不必担心破坏已完成的功能。使用这种开发方法,我们可以让设计更加优良,能编写出可测试的代码,同时还能避免在不切实际的假设基础上过度地设计系统。要得到这些好处,只需不断添加可执行测试,一步步地驱动设计,从而最终实现整个系统。

这本书就是讲如何实行这每个小步骤的。在后面的章节中,我们将会学到TDD的本质以及玄妙之处,学到如何利用TDD开发普通Java应用程序及企业级Java应用程序,以及如何用ATDD(Acceptance TDD,验收测试驱动开发,测试驱动开发核心理念的一个扩展)来驱动整个开发过程。在用测试驱动开发实现某个具体功能之前,我们将会首先编写功能测试或验收测试,从系统功能角度驱动开发过程。

TDD其实并不是一个新概念。很久以前,不少开发人员就认为编写测试不仅仅是为了验证系统正确性了。至今你还可以从他们那里听到这个故事:从那时候起,我们写代码之前都会先写测试……。而现在,这种开发方法有了自己的名字——TDD。本书的大部分内容都是关于“如何测试驱动”以及“测试驱动什么”的。这些知识可以被用到各种日常软件开发任务中。

不过,相对于主流开发方法,TDD仍然很新。就像今天的日常用品曾经都是十足的奢侈品一样,新的开发或设计方法,通常都是只有资深的开发人员才能拥有的高级货,而在很多年以后,等这些先驱证明了新方法确实有效,这种方法才会被广泛接受,成为开发必备技能。

我相信TDD会逐渐地变成主流开发方法,而且我认为,TDD已经开始变为主流开发方法了。希望本书能够推动这个过程。

在本书的开始部分,我们会先讨论现有的开发方法在开发软件过程中遇到的挑战。一旦对现存问题有了基本共识,我们将着手讨论如何用TDD或ATDD解决这些问题,同时也会学着使用一些支持此开发方法的工具。1.1 挑战:用正确的方法解决正确的问题

开发软件的目的是为协助组织的经营运作。作为一个专业的软件开发人员,我们的主要任务是向客户交付一个能够真正帮助他们提高工作效率并减少运作成本的系统。

回顾我多年的专业软件开发经验,参阅几十年来软件开发的文献记载,再看看整个世界范围内的技术人员对于开发方法的争论,我们不难看出很多开发组织本应向他们的客户交付更好的软件。换言之,我们现在交付的软件可不怎么好用。就算这些软件能够正常的工作,它们也没有真正解决客户的问题。从本质上讲,我们写出的代码并没有满足客户的真正需求。

接下来,我们来看看为什么“糟糕的代码质量”和“不能满足客户日益变化的需求”会妨碍我们给客户交付足够好用的系统。1.1.1 糟糕的代码质量

虽然软件开发行业已经发展了很多年,但是开发出软件的质量依旧存在问题。近些年市场越来越注重软件的及时上市,对软件产品的需求量越来越大,同时很多的新技术也不断涌现。在这种情形下,软件行业将不可避免地继续面临质量问题。

这些质量问题主要表现在两个方面:高缺陷率和低可维护性。在缺陷的泥潭中挣扎

缺陷会带来很多额外的开销,因为它会导致软件不稳定,行为不可预测,或者完全不能使用。缺陷减少了软件本身的价值,有时甚至使软件造成的破坏远大于创造出的价值。

测试可以解决这些问题——我们仔细地检查软件是否像期望的那样工作,同时也试着通过某种方式检测其是否稳定。测试在软件开发中的重要性是毋庸置疑的,但是传统的测试方法只会在整个软件开发完毕并且代码“冻结”后才进行,而且会耗费很长时间。这种测试方法有很大的改进空间。例如,在测试阶段修复一个缺陷的成本,通常是在编码阶段就修复这个缺陷成本的两倍或者更多。有缺陷的软件是不能交付的。我们在寻找并修复缺陷上用的时间越多,开销越大,我们开发软件的能力也越低。

软件缺陷通常是由低质量的代码引起的,要维护这些代码简直就是噩梦,而想对其进行进一步开发也会举步维艰,代价高昂。维护困难,开发缓慢

好的代码有很多优点:整体设计优秀,清楚地划分出了各部分之间的功能和责任,并且没有重复。而糟糕的代码可不是这样,成天和这种代码打交道简直就是噩梦。这些代码很难读懂,修改也很困难,因此工作效率会很低。改这里,还会莫名其妙地破坏掉那里的功能。重复的代码使修复缺陷也变得不容易,改完一处,还要找出其他所有重复的代码,挨个修正同样的缺陷才算完。糟糕的代码带来的问题还远不止这些。“我不想碰它,那任务永远不可能完成,如果做了,天知道会破坏掉其他什么功能。”这是软件业需要解决的一个很现实的问题。想修改或者添加新功能时,应该是基于现有系统继续开发,而不是重写,这就是可维护性的重要之处。有良好的可维护性,我们才能及时应对迅速变化的业务需求。如果代码的维护性差,那么我们行动和响应的速度会变慢,按时交付的压力会变大,这会促使开发人员写出质量更差的代码。如果想持续地交付软件,我们必须打破这种恶性循环。

这些问题已经够糟糕了,但是还有其他问题:我们交付的软件通常都没有完全满足客户的需求。1.1.2 不能满足客户需求

没人愿意花钱当冤大头,但软件行业的客户就经常被逼着这么做。在软件开发前期,客户和开发人员互相交换规格文档,然后开发工作就此开始。十二个月以后客户拿到系统,才发现这并不是自己当初想要的。更不用说在当前激烈的商战下,客户的业务需求和十二个月以前常常大相径庭。

由于交付的系统经常不能真正满足客户需求,软件开发行业一直在尝试用新的方法开发软件。我们试着在各种规格文档上多花些功夫,但效果却适得其反。我们试着延长交付系统的时间,却导致更多的人参与进开发过程。生产软件是为了支持人工作,但更多的人却为了生产软件而工作。另外,在开发初期确定过多的细节会导致文档不可靠。由于细节上的假设环环相扣,规格文件中的错误能轻易拖垮整个项目。

软件行业的业绩不佳,但也不必因此沮丧,因为这些问题有办法解决。敏捷软件开发——包括Extreme Programming(极限编程)和Scrum等方法——就是解决这些问题的良药。本书余下的内容会详细介绍敏捷开发方法的核心实践——测试驱动。1.2 解决方案:测试驱动

我们遇到的问题可以分为两个方面:代码质量不高和不能满足客户需求。解决方法也存在两个方面:学会正确地构建系统,以及学会构建正确的系统。本书所介绍的测试驱动方法能很相似地应用在这两方面,差别只在于如何利用测试来构建出维护性高并能真正满足客户需求的软件。

在细节层面上,我们用TDD方法以测试驱动的方式编写代码。在较高的,即软件的特性和功能层面上,我们使用类似的ATDD方法以测试驱动的方式构建系统。图1-1从提高内部质量和外部质量角度上描述了这种方法组合。

从图1-1中可以看出,在这两个不同的层面上结合使用测试驱动,能保证软件的内部质量,同时能保证可见的外部质量。在下面的小节里,我们将会讨论TDD和ATDD是如何带来这些改进的。在深入技术细节之前,先看看测试驱动如何帮我们应付软件开发中的挑战吧。图1-1 TDD用来提高软件的内部质量,而验收测试驱动开发保证开发出的软件能满足正确的功能需求1.2.1 高质量的TDD

TDD鼓励优良设计,这种纪律严明的方式还能帮我们避免在开发中引入错误。用TDD开发,我们会写很多小的自动化测试,这些测试最终会组成一个有效的预警系统以防止代码蜕化。没人能在现有代码中凭空加入“质量”,而提倡短开发周期的TDD从项目开始就能保证较高的代码质量,并一直延续下去。

这种短开发周期的开发方式与旧方式有很大不同。我们习惯于先设计,然后编码实现,最后做一些并不完备的测试。(我们都是优秀的程序员,很少犯错,所以稍加测试即可,不是吗?)TDD完全颠倒了整个过程。我们会先写测试描述出目标,然后写代码达到这个清晰的目标,最后再设计——在已有代码的基础上找出最简单的设计。

开发周期的最后一步叫作重构。重构是种严格的方法,用于改变代码结构,消除重复,并改良设计。我们能通过持续重构逐渐地提升代码质量。

如果你还不够了解TDD的完整周期,不用担心,我们将会在1.3节中详细介绍。

回顾一下TDD的特点:这种开发方法能帮我们写出完全可测试的代码,在每个阶段演化出当前最佳的设计,还能帮我们避免陷入代码越写越糟的恶性循环。

说到质量,下面我们来讨论“质量”这相对抽象概念的具体含义,以及它对我们而言意味着什么。不同形式的“质量”

正如业内质量保证部门所见证的,人们倾向于把“质量”与软件的缺陷数量联系在一起。“质量”也可能表示软件对客户需求的满足程度。还有人认为“质量”表示的不仅是可见的外部质量,还应包括内部质量(这又可以表现为外部质量,例如开发成本维护成本等)。TDD可以提高所有这些不同定义的“质量”,因为它本质上有助于改良设计及提高代码质量。

测试没能覆盖所有代码分支是缺陷流入产品的首要原因。(或者是因为我们有些懒惰,没有认真地执行所有测试——至少不太认真,所以让缺陷漏过去了。)

使用TDD,我们能保证所有的代码都是有用的,而且都会被测试覆盖。因为有高覆盖率的自动化测试,TDD能保证每个测试覆盖的功能都不会出现问题。因此,如何提高质量的问题就转化成了如何编写高质量测试的问题,解决了这个问题,质量问题也就迎刃而解了。

编写高质量的测试有一定的技巧,例如需要针对正常执行路径、边界值以及可能的错误操作分别做测试。在这方面,TDD可以促使我们从外在接口的角度考虑模块和类的设计。由于不用先考虑实现细节,我们可以站在更恰当的角度设想这个类的行为以及其他开发人员可能的使用方式,因而更容易发现代码中存在问题。

由于TDD关注高质量的代码及优良的设计,以往用于调试的大量时间,现在都被用在实现用户需要的功能及改良现有系统的设计上了。花更少的时间修复缺陷

TDD能帮我们缩短修复缺陷所用的时间。在引入缺陷的两个月后再去修复显然比起立即修复代价要大得多,因此有必要尽力减少引入缺陷的数量,尽快发现软件中已有的缺陷。

若能保证写代码时测试先行地小步前进,那么调试器也基本没用了,因为只有新添加的几行代码才可能破坏测试,所以找出问题会很容易,也不会像传说中的高手那样长时间地调试代码了。我们可以更快地修复缺陷,相应的开发成本也会降低。每个遗漏缺陷都可能造成几百甚至几万元的损失,可不是一笔小数目啊。不用连续几个小时调试程序,我们也有更多的时间做其他更有意义的事情了。

更快速地完成用户所需要的功能有很多好处:我们有更多的时间用于清理代码,学习最新的工具和开发技术,赶上其他优秀的同事,等等。这些会帮助我们提高代码质量,变得更加自信,做事也更快。这样一来,我们TDD又会变得更有效。这种良性循环带来的提升空间几乎是无限的。

我们一会儿将会讨论TDD给我们开发人员带来的更多实惠,不过在这之前,我们先介绍ATDD吧。1.2.2 用ATDD满足客户需求

TDD能从技术角度帮我们提高代码质量,使代码执行结果正确,容易理解,也容易维护。不过用TDD写出的代码的验证逻辑针对的是独立的代码块,而不是系统的具体功能。而且用测试先行的方法写出的最棒的代码也有可能做错事情,也许做出的功能不是客户想要的。因此,使用ATDD很有必要。要给系统添加新的功能,传统的做法是先写出需求文档,然后开发人员按照文档开发,完成后进行内部测试,最后交给客户做验收测试。ATDD则有些不同:我们会先写测试然后再实现功能,如图1-2所示。换言之,我们把客户需求转化成一系列可执行的测试,开发工作会基于这些测试,而不是基于开发人员各自对需求文档的不同理解。

ATDD拉近了最终用户和开发人员的距离,这是生产优秀软件的必要条件。在ATDD中,客户与开发团队会紧密地合作,定义出清晰的毫无歧义的测试,而不是花大量的时间写含糊不清的需求文档。这些验收测试能准确地告诉我们客户要求的功能是否已经完成。由于用户的需求全都转换成了可执行的具体的功能测试,因此可以确保开发出的软件切实地满足了客户的需求。图1-2 ATDD用自动化的可执行测试驱动新功能的开发

这个过程和代码层面上的TDD很相似。用ATDD开发软件,我们会更关注系统行为的测试,而不是对象行为的测试。因此我们也更需要使用客户和开发人员都理解的语言交流。

TDD和ATDD需要配合使用。在系统层面上使用ATDD驱动开发过程,而在每个功能点的实现层面上则使用TDD。这两种TDD方法之所以要结合使用,并不是因为存在紧耦合,而是因为这样可以使其互相弥补,互相支撑,而且能够更好地发挥各自优势。

我们现在应该已经了解了,TDD和ATDD如何能帮我们交付高质量且满足客户需求的软件。我们随即将会深入了解TDD是什么,为什么能够帮我们开发出高质量的软件,以及如何“用正确的方法做事”。在1.4节中,我们将会讨论如何用ATDD在更高的层面上来驱动开发过程以满足客户需求,即“做正确的事情”。在这以前,先来讨论我们这些开发人员从测试先行的开发方式中能获得什么好处。1.2.3 这对我有什么好处

没人会无缘无故地买辆新车,也没有人会仅仅因为开发方法够“新”就去使用它。要学一种新的工作方法必须有充分的理由,例如可以提高工作效率。我们现在已经知道使用TDD和ATDD可以帮我们提高代码质量,满足客户业务需求了。那么这种开发方法会如何让我们更享受工作呢?

根据我个人的使用经验,TDD起码有三大好处:

我很少会接到技术支持电话,就算有,也不会为了找出问题而花很长时间调试;

我对代码的质量很有信心;

我有更多的时间提高自己的专业素养。

下面来一一解释这些好处。不再长时间调试代码

直到现在,我还记得几年前接手的一项开发任务。公司内部有一个专有文件格式解释器,我需要修复文件解释器中的一个缺陷。我阅读了成百行的代码,在程序的各个部分间频繁跳转,最终才明白如何修复这个缺陷。

当时我修改了解释器的部分设计,以为既可以修复缺陷还能使代码更容易理解(那时候我还没有开始使用TDD)。实现新设计并且通过编译用去了几个小时。我怀着由那灵巧的设计带来的激动心情打开了终端窗口,在测试服务器上安装了解释器,然后发现它居然不能正常工作!我完全不知道原因。用调试器运行代码后还是不知道问题所在。我当时花费了数小时时间用调试器跟踪执行代码,最后才发现了问题——是个极小的错误。我筋疲力尽地离开办公室,心情很沮丧,责怪自己太粗心大意。

后来我意识到这不是因为粗心,而是完成任务的过程存在问题——步子跨得太大,导致注意力分散。如果能用TDD的方式写代码,把开发过程分解为小的、集中的测试,想必我能立即发现那个代码分支中的问题。

好像那恐怖的调试体验还不够糟糕似的,墨菲定律再次向我招了招手。不久我接到了客户怒气十足的电话,抱怨解释器在生产环境中崩溃了。我在修改解释器的设计时引入了至少一个严重缺陷。能找出更优的代码设计是一回事儿,而凌晨三点被愤怒的客户经理叫醒是另外一回事儿。(而这位经理被一个更恼火的客户叫醒。)

如果当时使用了TDD,或至少写了必要的测试,那个晚上我应该可以多睡两个小时的。这件事强烈地激发起了我写测试的意愿,因为突然意识到我过高估计了自己在工作方面的能力。对自己完成的工作很有信心

我们当然都希望写出的代码没有错误。如果代码中存在太多的问题饭碗就会难保。而另一方面我们又希望尽早完成工作,若在写代码上用去了过多的时间,日子同样也不会好过。因此,我们时常需要判断当前工作是否已经完成以及何时开始下项工作。

此刻,让我们打开记忆的匣子。回忆过去某个写代码时的场景,用一分钟回想起那个时刻。

你是如何写那段代码的?事先在记事簿上做设计?一下写完全部实现期望一次通过,否则就全部推倒重来?在循环中找出过明显的问题吗?首次编译能通过吗?

你如何验证某段代码能正常工作?专门为测试写一个主函数?在用户界面上点来点去确认功能已经实现?在测试中发现过问题吗?用调试器一步步跟踪代码执行?费尽周折只为修复一个很小的缺陷?调试代码和写代码大体上哪个更费时间?

无论答案是什么,我希望你已经基本回忆起那些让你充满信心的代码是如何编写出来的了。好,我来问你个问题。

如果你写出的代码完美无瑕,会不会感觉十分自信?如果知道代码正如文档中描述的那样正常工作,会不会感觉心情舒畅?我会的。如果你能很快地完成工作,同时保证代码质量没问题,这种感觉又如何?若能一直这样工作,会不会很开心?

我不能保证你使用TDD后开发出的软件就能完全没缺陷,因为最终还是你写代码,如何避免缺陷取决于你。我能保证的是,在使用TDD后,你会对你的软件很有信心,因为你明确知道软件在各种情况下会如何工作。

这个方法对提高软件内部质量同样有效。这可以说是个良性循环。测试越好,代码质量越高,对代码的改动也越有信心。对代码改动有了信心,能够改动的地方就越多。改动越多,代码的内部质量就越高,也更容易编写测试。这显然是件好事!有更多时间做其他事情

TDD和ATDD并不会提高我们敲键盘的速度,但能有效缩短由于低效开发方式(例如调试代码,理解可读性不高的代码,或因为误解需求而返工)所浪费的时间。随着开发一步步进行测试会不断累积,我们对代码质量也会更有信心。我们不再会怀疑代码在不同情况下会有不同的执行结果,也不会担忧一些数据的组合会使程序停止工作,因此也不必一遍遍重复地做测试。

对代码越有信心,我们就能越快地开始下一项工作。当然,有时自信是盲目的,不过即便如此,也会比这点一下在那输入点数据来确认功能是否正常要强的多。

TDD和ATDD不是“银弹”,但也可以算是分时系统以来最能提高软件开发效率的技术了。在下一节,我们会详细讲解TDD,然后讨论ATDD。

下面开始吧。1.3 正确地做事:TDD

TDD是一种开发和设计技术,可以帮我们增量式地构建系统,也能保证代码在任何时刻都不会错得离谱。测试正是我们小步前进的方法。

在本节中,我们将会学到TDD的工作原理,也会了解这种技术所带来的好处。我们先从TDD周期开始介绍,这是TDD的核心内容。然后我们会解释让软件从一开始就能正常工作的意义。若要增量式地构建整个系统,我们必须能只为当前做设计,而非一开始就设计完所有东西。我们会讨论如何用TDD实现增量式地构建系统。最后我们会讨论如何运用这种方法来保证软件每天都很“健康”,每天都能正常工作。

开始吧。接下来要讲解的是TDD的测试—编码—重构周期。1.3.1 测试—编码—重构

本章第一段曾经提到 TDD的原则十分简单:

写代码只为修复失败了的测试。

换言之,就是首先写测试,然后编码让测试通过。有些人会觉得这种方法和在学校学到的有些矛盾。学校教我们先做设计,接着实现,最后测试以找出代码中的问题。而TDD颠倒了整个过程,如图1-3所示。

测试先行,然后编码,最后做设计。这种“设计后行”的思维方法不奇怪吗?一点也不。这种设计有别于传统的“设计—编码—测试”过程中的设计。并且因为这两种设计区别很大,这种事后设计的方法甚至有了自己的名字——重构,表示把当前设计转换成一个更佳设计的过程。改名后的TDD周期如图1-4所示:测试—编码—重构。图1-3 TDD颠倒了传统的设计—编码—测试顺序。我们会先测试,然后编码,最后设计图1-4 让我们测试驱动开发人员着迷的测试—编码—重构过程。这幅图精确地描述了开发的过程,很容易理解,看起来也很酷

这小小的“测试—编码—重构”周期看似简单,其内部却蕴藏着巨大的威力。它足以改变每个人的软件开发过程的质量,从而提高整个团队、项目及组织的软件开发过程的质量。红—绿—重构TDD周期,即添加测试、编写代码通过测试及修改设计,也可以用“红—绿—重构”表示。这些颜色有什么含义?当我们进行TDD周期的第一步,添加一个测试时,测试会失败。这是因为这时候系统有问题,它并不具有我们所期望的功能。在一些开发环境中失败的测试会显示为一个红条,因此第一步为“红”。第二步是写代码通过测试。我们实现了系统应该有的功能,所有都测试通过了,包括为功能添加的新测试以及已经存在的测试。这时候红条变成了绿条,所以称为“绿”。周期的最后一步是重构,即改善现有代码的设计。因为重构只改变代码内部结构而并不改变外在行为,所以所有测试仍旧通过,还是“绿”。红、绿、绿,红、绿、重构,很上口,不是吗?

我们将会在第二章详细讨论TDD周期中的每一步,不过在这之前,我们会先大致了解如何做,以及为何做这三个步骤。然后我们会讨论其内在机理。先写测试

在TDD周期中的第一步中,我们会写测试,实际上这并不只是写测试而已,而是在做设计。我们是在设计API,即用来访问新功能的接口。编码之前写测试,我们会很自然地考虑新代码的调用方式。这过程就像拼图一样。我们必须根据拼图周围的部分来选择拼哪块,如图1-5所示。图1-5 若不试着使用,我们怎么会知道接口应当是什么样子?测试先行的编码方式会促使我们站在代码用户(开发人员)的角度考虑,设计出更易用的API

这并不是件容易的事。也许你从用户界面专家那里了解到了设计用户界面有多么重要。软件内部的设计又何尝不是如此?我们这些开发人员不就是代码的用户吗?

若从这种角度观察代码,我们的思路会发生很大变化。我时常呆望着一些第三方类库的API,思考着该如何使用它们。这些类库的开发人员肯定是站在开发者的角度设计接口的,完全没从用户的角度考虑。接口是好是坏用过才会知道。这绝对是真理。若能测试先行地开发,我们肯定会对类库的好坏有所体验。注意 写测试时应当注意粒度。应当尽量写“正好足以失败”的测试,而不是一下写出整个功能点的测试然后花一个小时写代码让测试通过。问题领域、工具或技术都会影响编写测试用的时间,不过通常编写测试的时间都在几秒钟到几分钟之间。编码的时间也应如此。如果用的技术较复杂,那么写测试及编码的时间可能会变长。不过在第二部分我们将会提到那些所谓复杂的技术(例如Java Servlet或数据访问的代码)实际并不那么复杂。

设计简单易用的API并不容易,因此我们要善于借助工具。用测试来驱动设计就是行之有效的办法,用这种方法能设计出模块化并且可测试的代码。因为要测试先行,所以我们必须要让代码可以测试,没有任何商量余地。在TDD中代码都应当是可测试的,否则根本不会存在!

设计软件并不是只强调结构,满足当前的需要更加重要。一个会烧水、煮饭、炸薯片,还会做腌鸡的软件对一个只想喝杯茶的用户一点用都没有。如果你的汽车引擎多出来两个阀门或许没什么,因为需要额外动力时这些阀门或许能帮上忙。不过若要更换引擎中的所有阀门那麻烦可就大了。这就是过度设计的代价。

花钱开发并不需要的东西,还要为额外的复杂性买单。如果这项功能目前尚不需要,为什么要开发呢?不如先将这项功能记在备忘录上好了。有些功能完全有可能永远不用实现。

使用TDD做开发,你会明确地知道软件现在需要有什么功能。不是明天也不是昨天,就是现在。小步前进,写代码只为通过测试,我们就可以牢牢地控制住软件及其设计。有了自动化测试做保护伞,我们不再会误入歧途,而且会很清楚前进的方向,同时也能够确信所实现的功能正是用户所需要的。“强调现在”正是TDD的核心。这核心思想会在TDD周期的第二步再次得到体现,即写恰巧足够的代码。写恰好足够的代码

编写恰好能通过测试的代码是TDD周期的第二步。为什么恰巧够就可以?新添加的测试之所以会失败,是因为它指出了系统应该有但尚未实现的功能。我们应该只用花几分钟就能实现这项功能,测试失败的状态不应持续太长时间。

让测试指出下一步该做的事情是TDD的基本理念。我们不仅仅是在制造代码,而是在实现一项具体的功能,而测试能毫无歧义地描述出这项功能。每次测试通过,我们都清楚地知道工作有了进展。

注意,写恰巧足够的代码是为了让测试尽快通过。因此当前的实现方式或许不是最优的。不过没关系,等功能实现、测试通过后,我们会回来解决这个问题的。有了测试做保护伞,就可以进行TDD周期中的最后一步——重构了。重构

重构是TDD测试—编码—重构周期的最后一步。我们回过头审视现有的代码设计,想办法改进。重构使TDD步伐更加稳健。使用TDD而不重构能迅速产生大量的烂代码。无论有多么充足的测试,烂代码终归还是烂代码。优良的代码质量能保证今后的开发效率,所以重构必不可少。这一步至关重要,所以我们需要用一整节详细讨论。

在这之前,我们先对小步地增量开发软件的方式做个概览吧。1.3.2 增量式开发

所有敏捷软件开发过程都有一个共同点,即无论当前的功能有多么少,都要保证软件可随时发布,并且每天都要能持续产生可部署的版本(有些项目甚至每天都会构建出几个可发布的版本,直到项目完成)。这样,当最后发布期限来临时至少有可用的产品能发布。虽然产品或许不包括用户想要的所有功能,团队也可能没有完成迭代计划,但是有产品可以发布总比没有强,更何况这产品运转良好。

在图1-6描绘的过程中,已完成且测试过的功能不断地累积。在每个时间点上,都只有少量的功能尚未完成或集成。

很多项目会不断地推迟交付时间,最后整个项目都会被取消,一行代码都未能交付。通过迭代式的小的增量式开发,你完全不用担心这个问题,因为从第一个迭代起软件就是可交付的。同样,很多的项目在交付时质量不过关,因为直到最后一刻开发人员还在赶工。

TDD可以解决这个问题。在TDD过程中,我们会小步地前进,每一小步都会产生一个工作良好的产品,每一步都会距离目标更近一些。这些步骤非常的小(以分钟计算,而非小时或天),我们完全不用担心刚写出来的一大堆代码拼在一起不能工作。我们从不让软件偏离可用状态太远,因此软件一直都能正常工作。同样,我们会着眼于现在而不是预测将来,这样就能保持软件精简实用了。图1-6 使用增量式开发,即以小的增量方式构建整个系统,我们的代码永远不会距离已集成的、可工作的状态太远。这样可以降低风险,因为未完成的工作一直都很少。我们以后将会学到,使用增量式开发,用户总能够看到真实的、可工作的软件,客户也能不断提出反馈,因此团队的工作也会更加高效

传统过程强调“事先设计所有东西,考虑所有的风险,这样架构才可以经得住考验,将来才能支撑起所有系统特性”。若采用这种方式,那我们是无法做到增量式构建软件,特别是依照成本和业务价值来增量式地构建软件的。传统的方式只能用于极其简单,或者每个细节都已经被透彻理解了的项目。而其他类型的项目则需要用迭代的办法一步步增量式设计。

如图1-7所示,在迭代、增量式的开发过程中,我们需要不断地在“实现新功能”和为支持新功能而“调整设计乃至架构”两项任务间来回切换。图1-7 增量式演化设计是指在系统不断添加更多的功能和行为的过程中,不断地微调代码结构。在代码生命周期的任何时刻,代码所展现的都是开发人员为完成现有功能所做的最好的设计。用这种方法,我们可以演化出能经受实践检验的架构

这就是增量式演化设计。我们只为完成当前功能而设计,而不会试图事先做完所有设计。按照开发过程中获得的信息调整当前设计,而不是在项目一开始的所谓设计阶段就企图预见到所有应用场景,然后基于这些或实或虚的假设做设计。

事先设计的量要依情况而定,需要考虑当前团队、个体,以及所采用的技术等因素。我们要时刻注意保持正确的方向,所做的设计大部分都不对?那就少做点事先设计。发现设计不容易扩展?那就多做些事先设计。

我们在文中常常提到“小步骤”这个词,下面来解释一下其优点吧。小到能够装进我们的脑袋

有两个原因促使我们把大的任务分解成许多小任务。首先,我们要解决的问题通常都比较大而且模糊,其复杂性也不易控制,因此我们需要把它分解成更容易解决的小问题。不知道你是否和我一样,至少我的脑袋不太擅长对付这种大的怪物。图1-8演示了如何把复杂的问题拆分成更简单的小问题,然后一个个解决。图1-8 要解决复杂的问题,先集中解决其中一小部分效果会更好

大部分人的工作记忆都只能同时处理5到7个概念。如果一次处理的信息太多,大脑就必须在工作记忆和长期记忆之间来回切换,因此难免遗漏部分信息。若能把整个解决问题的过程分解成一个个小的阶段性目标,整体进度也就有了具体的衡量标准。在开发过程中,由于整个过程划分成了一系列的测试,利用这些测试,我们完全可以度量出当前的开发进度。

在TDD过程中我们会演进式地设计系统,所以正好是小步地前进。我们会持续地改良系统设计,在开发过程中逐渐地演化出最适合当前需求的架构。

现在我们来仔细研究演进式设计为何行之有效,为何能让我们的代码“活”起来,以及对我们的工作带来的影响。演进式设计

很多程序员可能都遇见过这种事:某块代码亟待修改,却没有人愿意接手。为什么会这样?这段代码正巧是两个组件间的接口,修改工作太过困难。而在演进式设计中,我们常常会做这种修改。代码应当是“活的”并且是“可生长”的,决不能无视强烈的变化需求而保持一成不变。正因为如此,演进式设计可以提高设计质量,进而提高整个系统的质量。

那么,究竟该如何做演进式设计呢?演进式设计也是小步前进的。每种敏捷软件过程中推荐的事先设计的量都不同,不过这些过程都有一个共同点,即要求架构恰好能满足需要即可。例如你在开发某个软件,到了中期时遇到一个需求,要用到邮件服务器,直到这时候你才会往架构中加入邮件服务的构件,才会安装邮件服务器,等等。通常这种类型的架构改动很容易,但是并不是所有修改都是这样。

软件系统会有些非功能性需求,例如性能等,修改现有架构支持这类需求通常很不容易。例如把一个单机版的桌面应用程序改成能通过网络与多用户服务器通信的桌面客户端,工作量可不会小。同样,让一个批处理应用程序支持实时自动更新也不是件轻松的任务。

不过这类需求不会让开发人员多惊讶。虽然我们通常难以预测需求变化,但是根据现有需求预见将来的需求变化并不是不可能的。我们将这类变化称为可预见的变化,如图1-9所示。不过,可预见的变化并不一定会发生,有时甚至永远不会发生,还有的会因为某种原因而转化为不可预见的变化。图1-9 系统的演进式设计同时受“可预见”和“不可预见”两种变化的影响。不过值得注意的是,相当一部分“可预见”的变化都不会发生,或者以某种不曾预料到的方式出现,这时它们就变成了“不可预见”的变化。我们需要运用常识和敏锐的判断力,为可能发生的变化作准备。大部分这种变化或许永远不会发生

做演进设计时我们一样要使用常识,不过要明白事情是会发生变化的,同时要注意现有任务的优先级。

例如我们知道系统目前会通过网络从公司的CRM系统中更新信息,不过在几个月后更新过程会变为实时的,而且会用HTTP协议调用Web服务。在这种情况下,我们该如何应对实时数据集成带来的新需求呢?

我们应当把数据处理逻辑和数据接收逻辑分开吗?当然!那我们应当现在就开始做一个基于批处理的系统,等Web服务上线后直接就能投入使用吗?或许应该,或许不应该。

总之我们需要在“避免做无用功”和“先挑省事的方法做,等出了问题再说”之间做出权衡。实践一次又一次地证明,比起纸上谈兵的事先设计,演进式设计方法更加经济有效。遵守纪律

再强调一遍,每个项目要做的事先设计的量都不一样(原本就不该一刀切),但是在演进式设计中,我们总会为添加新需求而修改大量现有的代码。这种改动相当频繁,所以代码质量一定要高。为此我们需要不少严格的规章纪律,确保开发人员不会降低代码质量。幸好,演进式设计及其辅助实践可以帮我们解决这个问题。它能帮我们把缺陷数量降低到接近零的水平,而不会让整个系统构建在大堆隐藏的缺陷的基础上。

这些辅助性的实践究竟是什么?简言之,这些实践都是为了保证软件的质量在任何时候都足够好,而重构是其中很重要的一环。我们前面提到过重构是TDD周期,即测试—编码—重构的最后一步。下面我们会详细讲解重构的重要之处。1.3.3 重构以保持代码的健康

在小步前进的过程中,我们会不断扩展当前设计以支持新功能,也会不断抛弃旧概念,引进新概念。在这种情况下,软件的设计必然会变得很不一致,几经失衡,不易理解,也难以扩展。这些肯定会给我们交付软件的能力带来负面影响。不过不用担心,通过做重构,我们既可以演进式地设计,又能保持系统设计不出问题。

来看看Martin Fowler在他的著作《重构:改善既有代码的设计》中对重构的定义:“重构是一种训练有素、有条不紊的代码清理方式,它必须在不改变代码外在行为的情况下,改进程序的内部结构。”这句话虽然很短,但却传达了很多的信息,下面我们来详细解释这句话的含义。重构是种纪律严明的方法

当重构(动词)时,我们并不仅仅是修改代码而已,而是通过一种严格的方式改善现有设计。这种方式叫作重构(名词),它能小步地改变代码结构同时不改变代码的行为。换言之,重构是指用某种严格的方式重构(动词)代码。重构或许会显著地改变当前设计,不过这种改变总是小步进行的,而且每一步都会验证代码的行为没有改变。

我们可不仅仅是在修改代码。我们要首先确定设计中的问题,然后选择对应的重构方法小心彻底地重构这段代码。我们会静候问题浮出水面,然后再解决问题。我们并不会预测设计可能出现的问题,也不会为这种问题作准备——因为这不会使问题减少,反而会让事情变得更糟。重构是种转换过程

重构是两种状态间的转换。在初始状态中,代码的设计存在一定问题,而在目标状态中这些问题都已经被修复了。图1-10中的重构手法为“以委托取代继承”(请参考Martin Fowler的《重构:改善既有代码的设计》一书)。这种重构手法的目的在于把继承转化成委托。

若子类只想重用父类一小部分功能,但却继承了父类大部分我们并不需要的数据和功能,这时候就可以应用重构这种手法。

有些重构方法非常成熟,对很多现代开发工具都有很好的支持。把重构过程自动化使得用重构做演进式设计更容易了,无论项目的规模有多大或复杂性有多高。(很难想象仅仅为了给一个方法重命名而去搜索几十个源代码文件,用查找替换功能一处一处改名,然后再把改动提交到版本控制系统中去。)图1-10 重构是指代码在功能完全相同的两个状态或结构之间的转换。在图中,我们用委托替代了继承。转换后代码功能不变,但却更符合我们当前的设计需要。这些转换不是绝对的改进,而仅仅是用户两种设计方案互相转换的“纪律严明”的方法而已。很多重构手法都有其反向重构手法,重构的方向正好相反重构到模式有时候重构的起始状态或结束状态可能会对应某个设计模式。所谓设计模式就是对特定问题的通用解决办法。虽然我们日常的重构动作都很小,并没有像使用设计模式那样会改动大量的代码,不过重构过程中也需要发掘代码中可能的设计模式,然后显式地重构出模式。要深入了解重构和模式之间的关系,请参阅Joshua Kerievsky所著的《重构与模式》。重构会改变内部结构

重构用于改变系统内部结构——代码,因此大部分重构手法都会涉及实现细节,例如应用最为频繁的重构手法“重命名”。给方法或者局部变量重命名对系统的设计貌似不会有太大影响,但是若把某个含义模糊的名字改得能清晰地表达出本意,那么对阅读代码的人就会轻松多了。

这些小的重构是更大的重构的基础。在“大重构”中,我们通常会改变代码的职责,引入或移除继承结构,或者做一些会涉及几个类的类似操作等。最终大重构都可以分解为一系列小步骤,例如给变量重命名,添加一个新的类,改变方法返回值,等等。

虽然从技术角度来看“重命名”等重构方法使用率最高,但重构最主要的却是为了消除重复。重复是指两个方法或类中有相似的代码。我们可以把重复的代码提取到公共方法中,然后再调用这个方法来消除重复。若代码中存在重复,表明代码职责划分得不够清楚。重复对系统当然是有害的,因为它使系统变动更麻烦。让逻辑和职责在不同地方重复一定会引入缺陷,因为我们很容易修改一处而忘记其他的。

重构时不仅不能引入缺陷,也不能添加新功能。重构应当保持系统原有行为。重构保持原有行为

Martin Fowler提到重构应该“不改变代码的外部行为”,这是什么意思?这是指无论怎么修改代码,所变化的都只有代码的设计和内部结构,外部可见的行为和功能会维持原样。换言之,代码的用户应当对重构毫无察觉。

改变一个类的公共方法的名字,肯定会影响到调用这个方法的代码,但是这个方法的行为不应该发生改变。同样,调整公共接口背后的代码也不会改变接口提供的功能。注意 现在我们已经知道了什么是重构,不过,知道什么不是重构也很重要。这些年我常听到“重构”这个词,不过在大部分情况下,人们只是想表达“重写”或“编辑”而已。

重构时只能改变系统内部结构而不能改变系统外部行为,对吗?那我们怎么知道真的没有改变系统的外部行为呢?当然是靠测试了!测试可以确保软件正常运行。1.3.4 保证软件正常运行

保证从项目第一天起就能交付软件,说起来容易做起来难。既要不断重构代码,又要保证重构工作不破坏已有的功能,可不是件容易的事。在无人督促时,我们有可能会偷懒。虽然TDD中测试的主要目的是帮助我们设计和开发软件,不过这种方法也能督促我们坚持正确的做事方式。

测试并不是负担,不过许多人都不这么认为。手工测试的确很慢,也容易出错,因此开发人员(甚至专业测试人员)都不喜欢做测试,总会跳过某些手工测试,猜想待测功能不会有问题。不过手工测试的缺点是很容易克服的,把手工劳动转化为自动化形式就是我们软件开发人员的本职工作,因此把手工测试变成自动化测试完全难不倒我们!用自动化测试做保护

回归是指返回到更初级的状态。在软件开发领域,回归表明已有的、曾经可用的功能不再能正常运转,即从正常工作的状态退化到不能工作的状态,如图1-11所示。图1-11 测试套件在代码周围形成了一个模子。若改动破坏了功能,模子就不再符合了。若模子破损了,我们也就知道不好的事情发生了

回归不会平白无故地发生。实际上,是我们开发人员自己修改代码时引入了缺陷才会出现回归。作为专业软件开发人员,我们当然希望能迅速知道当前改动是否会破坏现有功能,因此必须依靠测试套件。我们可以随时执行所有测试,若测试失败了,就表明某项功能出现了问题。

这种测试称为回归测试,在整个开发过程中,回归测试会被反复执行许多遍,以保证以前发现的缺陷不会在系统发生上千次修改及变化后再次出现。换言之,这种测试是为了保证自上次运行测试以来,软件没有发生回归。

测试套件有许多重要特征,首先其必须能容易运行——否则人们就会试图绕过测试,冒着破坏现有功能的风险提交代码。另外还要能快速地执行——否则人们就不会常常执行测试,因此又会跳过很重要的功能验证步骤。

若不频繁地执行测试,那么将来我们可能花费很多宝贵的时间修复缺陷,因为那时完全不知道缺陷是在哪一步被引入的。及时反馈所带来的正确上下文,可以帮我们明显提高开发速度,因为不需要花时间找出引入缺陷时的上下文。如果我们在每次微小变动后都运行自动化测试,那么我们可以精确地知道出错的是哪几行代码。快速获得反馈

有时我们没法很快地执行完所有测试。那些访问数据库、共享磁盘、Internet上的Web服务器或者目录服务的测试会让测试速度变得很慢,甚至连访问本地文件系统都会使整个测试多运行数分钟时间。而且有时候测试次数太多,就算优化很多次依旧会占用开发人员大量的时间。

在这种情况下,可以选择运行一部分测试(通常是最可能发现当前改动所引发缺陷的那部分测试),提交代码,继续下一项工作,让构建服务器(build server)在后台运行所有测试。构建服务器有时称为持续集成服务器,因为它经常和“持续集成”一起使用。所谓持续集成是指开发人员频繁地集成修改过的代码,使得集成几乎是“持续”的。拥有持续集成服务器并不等于做持续集成不应该把拥有持续集成服务器与进行持续集成等同起来。持续集成服务器可以使构建过程自动化,还能产生精美的报表。但如果开发人员很少提交代码,那么持续集成服务器就会形同虚设,开发人员的代码也会变得不同步,集成时当然会碰到更多的代码冲突。

这种方法基本上是用“上次修改代码没有破坏任何功能”的信心来交换开发速度的,而这种交换则建立在开发人员挑选的部分测试没有覆盖的功能不会被该修改破坏的假设之上。

我们可以把这种方式想象为快速获得反馈的“乐观锁”。如果测试没有失败,我们可以只关注于开发,完全不用花时间去运行那些不会失败的测试。如果让构建服务器运行的测试失败了,那我们需要用更多的时间找出问题所在,因为手中的任务已经换了,问题发生时的上下文也改变了。我们需要做出取舍,因为若没有问题发生的上下文,修复缺陷的时间和精力会大大地增加。

回归测试必须要能够重复执行。也就是说我们要么雇人用大量时间重复执行成千上万的回归测试,要么自动化所有的测试,让计算机来自动地执行回归测试。

我们现在已经明白了TDD是什么,如何运作,也明白了采用这种技术的原因。我们将会在第一及第二部分的剩下章节中详细讲解这种技术,不过在这之前,我们会先了解验收测试驱动开发的基本功能,以及如何能帮我们开发出满足客户需求的软件。1.4 做正确的事:ATDD

测试一直都是软件开发过程中重要的一环,测试的具体方法这些年发生了很大的变化。在把软件交付给客户之前,我们开发人员都会想尽办法确保交付的软件没有问题。随着敏捷软件开发(如XP)过程的逐渐流行,我们检测软件质量的方法也发生了不少变化。

测试曾经只是为了验证软件功能和需求文档所描述的一致,不过现在测试的功能已经不局限于此了。实际上,利用测试从功能的角度驱动软件开发过程,正是我们解决软件不能满足客户真正需求问题的方法。这就是验收测试驱动开发(ATDD)的内容。简言之,我们会先写一个测试,然后再实现测试所描述的功能。

因此,测试不仅仅是一种验证工具,还是需求文档必不可少的一部分,同时也是与客户协作的媒介。在本节中,我们会详细讨论测试的这些新用途,先从促进开发人员、测试人员以及客户之间的交互开始谈起,然后讨论如何把测试当作能促进协作的“共同语言”。

在讨论ATDD促进协作等特征之前,我们先来理清ATDD和TDD之间的关系吧。这两者名字很相似,必定存在某种联系。1.4.1 名字的含义

从ATDD的名字“验收测试驱动开发”中可以看出,这种技术与“测试驱动开发”有一定关系。“验收测试驱动开发”中的“测试驱动开发”部分显然源自TDD,那么剩下的部分究竟有何含义?验收测试是什么?简单地说,验收测试是用来检测某项功能的完成情况的。如果某个功能的所有验收测试都通过了,那么这个功能也就完成了。

无论用何种格式表述需求,都可以采用ATDD。我们可以把需求记录在用户用例、用户故事或者其他文档上,都没关系。有时采用用户故事管理需求的团队倾向将这种方法称为“故事测试驱动开发”,实际上还是指同一种技术。还有人喜欢称其为“客户测试驱动开发”,这样也可以,因为验收测试的所有权本来就是客户的。注意 尽管ATDD很像TDD,也借鉴了TDD的许多优点,但这两种开发方法可以分开使用。那些不使用用户故事的开发人员,或者实现功能前不先写测试的开发人员,仍然可以使用TDD。那些不用测试先行的方式写代码的团队,仍然可以用ATDD测试先行地实现新功能。这些技术互相弥补互相支撑,结合使用时更能发挥其作用。

无论我们用什么格式或工具来管理需求,ATDD的主要目的都是促进客户和开发团队之间的紧密协作。下面我们来讨论测试驱动方法为何会有这种作用。1.4.2 紧密协作

在任何由人参与的复杂活动中,紧密协作都至关重要,采用ATDD来开发软件的活动也是如此。我们期望团队能够像一个整体,而不是开发团队、业务分析团队和测试团队截然分开,更别说设立一个单独的质量保证部门了。

要想使整个团队的生产力达到最高,团队成员之间必须能有效沟通,能面对面地讨论如何构建高质量的软件。若客户、测试人员、业务分析人员和开发人员之间用测试计划、需求文档和测试报告来交流和沟通,那可不行。使用ATDD,我们可以把知识、技能和能力汇集到一起,有效地协同工作。

下面我们来看看为什么这种紧密的协作能提高效率,消除对需求的误解,减少返工所带来的开销,进而帮助我们开发出真正满足客户需要的软件。看得见摸得着的软件

有些团队会闷头数月开发软件,完全不与客户沟通,这样开发出来的软件很少能让客户真正满意。若能不断地向客户展示已完成的功能,当软件中有些功能不正确或者与客户需求不符时,我们会立即得到反馈。若能尽早得到反馈,那么我们就能减少项目风险,降低成本了。此外,若能把所有完成的功能都展示给客户看,那么我们会清楚地了解当前进度,而不会像基于文档开发方式那样常常乐观地误认为任务“已经完成90%了”。建立信任及信心

尽早以及频繁地交付软件还可以在团队和客户之间,以及团队内部建立起信任感。通过向客户(以及自己)展示已完成的功能,整个团队工作起来会更轻松。客户做主

增量式软件开发过程中的客户权利,与传统的瀑布式开发过程中的客户权利有很大差别。在增量式开发过程中,客户有权决定哪些功能要先开发。同样,若团队不能在既定的预算和时限内完工,客户也可以取消某些功能。

功能的开发成本当然会影响到客户的决策。开发成本由开发人员估算,包括推迟开发的成本,以及改变开发顺序后的开销,等等。

若用户可以控制自己的钱花在哪些功能上,那么他们对待项目的态度也会发生变化。最终由客户决定他们的钱花在哪些功能上,绝对可以激发起客户的热情。培养共同语言

通过鼓励测试人员、开发人员以及客户之间的沟通协作,我们可以营造出一个氛围,在这个氛围中,有价值的信息可以在团队中迅速得以分享。此外,随着团队成员间不断地沟通,相互之间也会更了解,同时也会慢慢培养出共同的语言,这样沟通效率会变得更高。软件开发是人参与的活动,无论如何都不能忘记这一点。

我们好好想想,把验收测试作为整个团队(包括客户、测试人员及开发人员等)沟通时使用的共同语言的基础,是否可行呢?1.4.3 把测试作为沟通的共同语言

需求不清是软件开发中最大的问题之一。要清晰地表达出需求,保证需求在记述过程中还能保持原样,绝不是件容易的事情。有些人甚至认为这根本不可能,毕竟我们不会读心术。

当用文档(例如需求文档)作为沟通的媒介时问题尤为突出。文档绝对不是传递信息和交流的好方法。若我们能把客户需求转化成可执行测试,通过测试来验证系统是否满足客户的需求,那么问题就会少很多。这就是“以测试为规约”的好处。以测试为规约

若把测试当作需求的严格表述形式,那么至少在理论上能通过所有测试的系统一定能够满足客户的需求,当然测试一定要充分覆盖系统的每个部分才行。不过在现实的软件开发项目中,要做到这一点并不容易。

测试不会发现所有的缺陷,有一定商业软件项目经验的人绝不会因此感到困惑。部分原因是我们漏掉了一些应该想到的测试,还有一部分原因是人类的本性和常常欺骗我们的直觉促使我们跳过一些测试以节约时间。

如果测试无论如何都不会彻底,那么把测试当作规约还有意义吗?测试真的可以描述需求,定义概念吗?当然把测试当作规约,不会解决所有的问题,但其确实也有一些显著的优点:

可以自动化执行,更快地提供反馈;

能更可靠地执行测试;

少一个翻译的步骤。

首先,许多费时费力的测试工作都可以让自动化的可执行测试来做,比起手工测试,自动化测试还能显著缩短反馈周期。其次,计算机不会感到疲倦,也不会偷懒,让它帮忙做测试能够避免我们人类与生俱来的人性弱点。再次,反正把需求记录在文档上注定会有问题,现在只不过把记录需求换成记录测试用例而已,事情还能糟糕到哪去呢?

虽然把需求转化成测试的过程中仍然可能出现问题,但是在执行测试时知识转化量越少,需要人解读的地方越少,出错的机会就越少。以例子为规约

此外,以测试为规约的最显著的好处在于可以采用“以例子为规约”方式来描述需求,而不是用抽象的描述(当然了,在表述需求时,总是要有部分描述性文字的)。换言之,以例子为规约的需求表述方式提倡用例子表达需求,例如“若订阅价格为$20,税率为10%,那么系统应当从用户账户中共收取$22”,而不是用传统的需求文档中常见的“系统应当计算税”这种表述形式。

对于简单的需求,基于例子的方法并不比传统的“系统应当这样……”的方法好多少,毕竟我们都知道简单的计算税金的方法。然而并不是所有功能都这么简单,对于复杂的业务逻辑,误解的可能性

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载