代码整洁可用(clean code that works),Ron Jeffries这句言简意赅的话,正是测试驱动开发(TDD)所追求的目标。代码整洁可用之所以是一个值得追求的目标,是基于以下的一系列原因:
§ 它是一个可预测的开发方法。你知道什么时候可以完工,而不用去担心是否会长期被bug困扰。
§ 它给你一个全面正确地认识和利用代码的机会。如果你总是草率地利用你最先想到的方法,那么你可能再也没有时间去思考另一种更好的方法。
§ 它改善了你的软件用户的生活
§ 它让软件开发小组成员之间相互信赖
§ 这样的代码写起来感觉很好
但是我们要怎样做才能使代码整洁可用呢?很多因素妨碍我们得到整洁的代码,甚至是可用的代码。无需为此征求很多的意见,我们只需用自动运行的测试来推动开发,这是一种被称之为测试驱动开发的开发方式(TDD)。在测试驱动开发中,我们要这样做:
§ 只有自动测试失败时,我们才重写代码
§ 消除重复设计,优化设计结构
这是两条很简单的规则,但是由此产生了复杂的个人和小组行为规范,技术上的含意是:
§ 我们必须通过运行代码所提供的反馈来做决定,并以此达到有机设计的目的。
.
§ 我们必须自己写测试程序,这是因为测试很多,很频繁,我们不能每天把大量的时间浪费在等待他人写测试程序上
§ 我们的开发环境必须能迅速响应哪怕是很小的变化
§ 为使测试简单,我们的整个规划必须是由许多高内聚、低耦合的部分组成
这两条规则实际上蕴含了开发过程中所经历的阶段:
§ 不可运行──写一个不能工作的测试程序,一开始这个测试程序甚至不能编译
§ 可运行──尽快让这个测试程序工作,为此可以在程序中使用一些不合情理的方法
§ 重构──消除在让测试程序工作的过程中产生的重复设计,优化设计结构
不可运行/可运行/重构──这就是测试驱动开发的口号
现在假设这样的开发方式是可能的,那么,进一步,显著地减少代码的错误密度(defect density),让所有参与某一工作的开发人员对工作主题足够明了的假定也将成为可能。如果是这样的话,那么只有测试失败时才需要重写代码,其社会意义是:
§ 如果代码的错误密度能够充分地减少,那么软件的质量保证(QA)工作可以由被动保证软件质量转变为主动保证软件质量
§ 如果开发过程中令人不快的意外能够充分地减少,那么项目经理能对软件开发进度有一个精确的把握,以让实际用户参与日常开发
§ 如果每次技术讨论的主题都足够明确,那么软件工程师之间的合作是以分钟计算的,而不是每天合作或每周合作
§ 再者,如果代码错误密度能够充分地减少,我们每天都可以得到有新功能的软件成品(shippable),并以此产生新的商业关系
如此说来,观念是很简单的,但我的动机是什么呢?为什么一个软件工程师要做额外的写自动测试程序的工作?为什么一个设计观念可以瞬息万变的软件工程师却只能一小步一小步地进行工作?我们需要的是勇气。
勇气
测试驱动开发是一种可以在开发过程中控制忧虑感的开发方法。我并非指那些毫无意义的没有必要的担忧──(pow widdle prwogwammew needs a pacifiew)──而是指合理的担忧,担忧是否合理是个很困难的问题,不能从一开始就看出来。如果说疼痛自然就会叫 "停!",那么担忧自然就会说"细心!",小心谨慎是好的,但它也会产生以下一系列负面影响:
§ 让你一直处于试验性的阶段。
§ 让你不愿意与他人交流。
§ 让你羞于反馈。
§ 让你变得脾气暴躁。
这些负面影响对编程都是有害无益的,尤其是当需要编程解决的问题比较困难的时候。所以问题变为当我们面临一个比较困难的局面的时候,如何才能做到:
§ 尽快开始具体的学习,而不是一直处于试验性的阶段。
§ 更多地参与交流和沟通,而不是一直拒不开口
§ 寻找那些有益的、建设性的反馈,而不是尽量避免反馈
§ (依靠自己改掉坏脾气)
设想把编程看成是转动曲柄从井里提一桶水上来的过程。如果水桶比较小,那么仅需一个能自由转动的曲柄就可以了。如果水桶比较大而且装满水,那么还没等水桶全部被提上来你就会很累了。你需要一个防倒转的装置,以保证每转一次可以休息一会儿。水桶越重,防倒转的棘齿相距越近。
测试驱动开发中的测试程序就是防倒转装置上的棘齿。一旦我们的某个测试程序能工作了,你就知道,它从现在开始并且以后永远都可以工作了。相比于测试程序没有通过,你距离让所有的测试程序都工作又近了一步。现在我们的工作是让下一个测试程序工作,然后再下一个,就这样一直进行。分析表明,要编程解决的问题越难,每次测试所覆盖的范围就应该越小。
我的《Extreme Programming Explained》一书的读者可能会注意到我讲极限编程(XP)与讲测试驱动开发的语气是有区别的:讲测试驱动开发不像讲极限编程那么绝对。讲极限编
程时我会说"这些是想进一步学习所必须具备的基础",而讲测试驱动开发时要模糊一些。测试驱动开发知道编程过程中的反馈与欲实现的构思之间的差距,并且提供了控制这个差距大小的技术。"如果我在纸上规划一周,然后通过测试驱动编码,这是否就是测试驱动开发?"当然,这就是测试驱动开发。你知道欲实现的构思与反馈之间的差距,并且有意识地控制了这个差距。
绝大多数学习测试驱动开发的人发现他们的编程习惯被永久地改变了。"测试感染"(Test Infected)是Erich Gamma所杜撰的用以描述这种转变的词语。你可能发现写测试程序变得容易了,并且相对较小的工作节奏比前所梦想的节奏更明智。另一方面,一些学习测试驱动开发的软件工程师重回到了以前的程序开发方法,而保留测试驱动开发方法作为当其它开发方法不能奏效的特殊情况下的秘密武器。
当然也存在一些编程任务不能仅仅(或者根本就不能)由测试程序来驱动开发。举个例子来说,软件的安全性和并行性,测试驱动开发方法就不能充分地从机械证明的角度说明软件是否达到这两个目标。软件安全性从本质上来说依赖于无缺陷的代码。确实如此,但它同时也依赖于人们对软件安全机制的判断。精妙的并行问题不是仅靠再次运行代码就能可靠地重复出现的。
一旦你读完本书,你要准备:
§ 从简单的例子开始。
§ 写自动测试程序。
§ 重构,每次增加一个新的设计构思。
这本书是由三个部分组成的:
§ 第一部分,资金实例(The Money Example)── 一个典型的完全由测试驱动的代码模型的例子。这个例子是几年前我从Ward Cunningham那儿得到的,并且自从引入多币种算法以来已经多次用到过。你将从中学会如何在写代码之前写好测试程序,并最终发展成为一个有机的规划方案
§ 第二部分,xUnit实例(The xUnit Example)── 一个通过建立自动测试框架来测试包含映像(reflection)和异常(exception)的逻辑上更复杂的程序的例子。这个例子同时也将向你介绍作为许多面向程序员的测试工具灵魂的xUnit结构体系。在第二个例子中你将学会以甚至比第一个例子更小的开发步骤工作,同时也包括深受许多计算机专家喜爱的呼喊式的自我提醒(self-referential hooha)。
§ 第三部分,测试驱动开发模式(Patterns for Test-Driven Development)──包括决定写哪些测试的模式,如何用xUnit写测试的模式和大量的设计模式精选以及例子中所用到的重构。
我写了关于结对编程(pair programming)的例子。如果你习惯于在四处转一转之前先看一看地图的话,你可以直接到第三部分去看那些模式,并将那些例子作为说明。如果你习惯于先到四处转一转,然后再看地图以确定自己处于什么位置的话,试着通读例子,当你需要了解更多关于某一技术问题的细节时,可以查阅后面所讲的模式,并将这些模式作为参考。一些批评家指出,当他们启动编程环境,输入代码,运行所读到的测试程序时,最大的收获却在这些例子之外。
关于这些例子要注意一点。这两个例子,多币种计算和测试框架,显得很简单。而解决同一个问题却也存在(我曾经见到过)一些复杂、风格很差、近乎弱智的解决方案。我本可以从这些复杂、风格很差、近乎弱智的解决方案中采用一个以使本书有一种"真实"感。然而,我的目标是写出整洁可用的代码,希望你的目标也是这样。在以那些被认为很简单的例子开始之前,花15秒的时间设想一下,如果所有的代码都能如此清晰和直接,没有复杂的解决方案,只有显然需要认真思考的很复杂的问题,那么这个世界会是什么样子。测试驱动开发可以引导你这样去认真思考。