??xml version="1.0" encoding="utf-8" standalone="yes"?> 多年来,我惊叹于有如此大量能够 Microsoft Word 崩溃的坏文g。少数字节错位,会整个应用E序毁于一旦。在旧式的、无内存保护的操作系l中Q整个计机通常p样宕掉了。Word Z么不能意识到它接收到了坏的数据,q发Z条错误信息呢Qؓ什么它会仅仅因为少数字节被损坏q坏自q栈、堆呢?当然QWord q不是惟一一个面对畸形文件时表现得如此糟p的E序?/p> 本文介绍了一U试N免这U灾隄技术。在模糊试中,用随机坏数据Q也U做 fuzzQ攻M个程序,然后{着观察哪里遭到了破坏。模p测试的技巧在于,它是不符合逻辑的:自动模糊试不去猜测哪个数据会导致破坏(像人工试员那PQ而是尽可能多的杂ؕ数据投入E序中。由q个试验证q的p|模式通常对程序员来说是个d的震憾,因ؓM按逻辑思考的人都不会惛_q种p|?/p> 模糊试是一简单的技术,但它却能揭示出程序中的重?bug。它能够验证出现实世界中的错误模式ƈ在您的Y件发货前Ҏ在的应当被堵塞的d渠道q行提示?/p> 模糊试如何q行 模糊试的实现是一个非常简单的q程Q?/p> 可以用Q意多U方式改变该随机数据。例如,可以整个文件打乱,而不是仅替换其中的一部分Q也可以该文g限制?ASCII 文本或非零字节。不用什么方式进行分Ԍ关键是将大量随机数据攑օ应用E序q观察出故障的是什么?/p> 可以手动q行初始化测试,但要惌到最佳的效果则确实需要采用自动化模糊试。在q种情况下,当面临破坏输入时首先需要ؓ应用E序定义适当的错误行为。(如果当输入数据被破坏Ӟ您发现程序正常运行,且未定义发生的事Ӟ那么q就是第一?bug。)随后随机数据传递到E序中直到找C一个文Ӟ该文件不会触发适当的错误对话框、消息、异常,{等。存储ƈ记录该文Ӟq样p在稍后重现该问题。如此重复?/p> 管模糊试通常需要一些手动编码,但还有一些工兯提供帮助。例如,清单 1 昄了一个简单的 Java?c,该类随机更改文g的特定长度。我常愿意在开始的几个字节后面启动模糊试Q因为程序似乎更可能注意到早期的错误而不是后面的错误。(您的目的是想扑ֈE序未检到的错误,而不是寻扑ַl检到的。) 模糊试文g很简单。将其传臛_用程序通常不那么困难。如 AppleScript ?Perl 脚本语言通常是编写模p测试的最佳选择。对?GUI E序Q最困难的部分是辨认出应用程序是否检出正确的故障模式。有Ӟ最单的Ҏ是让一个h坐在E序前将每一个测试通过或失败的l果都标C来。一定要所有生成的随机试用例单独地命名ƈ保存下来Q这样就能够重现q个q程中检到的Q何故障?/p> 防护性编?/strong> 可靠的编码遵循了q样的基本原则:l不会让E序中插入未l过一致性及合理性验证的外部数据?/p> 如果从文件中d一个数字ƈ期望其ؓ正数Q那么,在用其q行q一步处理前对其先验证一下。如果期望字W串只包?ASCII 字母Q请定它确实是q样。如果认为文件包含一个四字节的整数倍的数据Q请验证一下。一定不要假设Q何外部提供的数据中的字符都会如您所料?/p> 最常见的错误是做出q样的假设:因ؓE序该数据写出Q该E序p不用验证再一ơ将该数据读回去。这是很危险的!因ؓ该数据很可能已经被另一个程序在盘上复写过了。它也可能已l被一个故障磁盘或坏的|络传输所破坏了或已经被另一个带 bug 的程序更改过了。它甚至可能已经被故意更改过以破坏程序的安全性。所以不要假设Q何事Q要q行验证?/p> 当然Q错误处理及验证十分令h生厌Q也很不方便Qƈ被全世界E序员们所轻视。计机的诞生已q入了六十个q头Q我们仍旧没有检查基本的东西Q如成功打开一个文件及内存分配是否成功。让E序员们在阅M个文件时试每一个字节和每一个不变量g是无望的 —?但不q样做就会ɽE序易被模糊d。幸q的是,可以L帮助。恰当用现代工具和技术能够显著减d固应用程序的痛苦Q特别是如下三种技术更为突出: 用校验和q行的模p试?/strong> 能够保护E序抵M模糊d的最单的Ҏ是将一个检验和d到数据中。例如,可以文件中所有的字节都篏加v来,然后取其除以 256 的余数。将得到的值存储到文gN的一个额外字节中。然后,在输入数据前Q先验证验和是否匚w。这简单模式将未被发现的意外故障的风险降低到约 1/256 ?/p> 健壮的校验和法?MD5 ?SHA q不仅仅取其除以 256 的余敎ͼ它完成的要多得多。在 Java 语言中,java.security.DigestInputStream ?java.security.DigestOutputStream cMؓ一个校验和附属到数据中提供了便L方式。用这些校验和法中的一U可以将E序遭受意外破坏的机率降低到于十亿分之一Q尽故意攻M有可能)?/p> XML 存储及验?/strong> 数据以 XML 形式存储是一U避免数据损坏的好方法。XML 最初即着力于 Web 面、书c、诗歌、文章及怼文档Q它几乎在每个领域都获取了巨大的成功Q从金融数据到矢量图形到序列化对象等{?/p> ?XML 格式抵制模糊d的关键特征是一个对输入不做M 假设的解析器。这是真正惛_一个健壮的文g格式中所获得的。设?XML 解析器是Z让Q何输入(格式良好的或无格式的Q有效的或无效的Q都以定义好的Ş式处理。XML 解析器能够处理Q?字节。如果数据首先通过?XML 解析器,则仅需要准备好接受解析器所能提供的东西。例如,不需要检查数据是否包含空字符Q因?XML 解析器绝不会传送一个空倹{如?XML 解析器在其输入中看到一个空字符Q它׃发出异常q停止处理。当然还需要处理这个异常,但编写一?catch 块来处理到的错误比L写代码来所有可能的错误来说要简单得多?/p> ZɽE序更加安全Q可以用 DTD ?或模式来验证文档。这不仅查了 XML 是否格式良好Q而且臛_与所预期更加接近。验证ƈ不会告知关于文档所需了解的一切,但它却ɾ~写大量单检查变得很单。用 XMLQ很明显能够所接受的文档严格地限定够处理的格式?/p> 管如此Q还有多D代码不能用 DTD 或模式进行验证。例如,不能试发票上商品的h是否和数据库中库存商品的h一致。当从客h收到一份包含h格的订单文档Ӟ不论其是 XML 格式或是其他格式Q在提交前通常都会查一下,以确保客户ƈ未修改h根{可以用定制代码实现q些最后的查?/p> Z语法的格?/strong> ?XML 能够Ҏp攻d有如此的抵M能力的是其用巴U斯-诺尔范式QBackus-Naur FormQBNFQ语法仔l且标准地定义的格式。许多解析器都是使用?JavaCC ?Bison {解析器-生成器工L接从此语法中构徏的。这U工L实质是阅M个Q意的输入ƈ定其是否符合此语法?/p> 如果 XML q不适合于您的文件格式,您仍可以从基于解析器的解x案的健壮性中L。您必须为文件格式自行编写语法,随后开发自q解析器来阅读它。相比用唾手可得的 XML 解析器,开发自q解析器需要更多的工作。然而它是一个更为健壮的解决ҎQ而不是不Ҏ语法正式地进行验证就数据简单地装蝲到内存中?/p> Java 代码验证 由模p测试导致的许多故障都是内存分配错误及缓冲器溢出的结果。用一U安全的垃圾攉语言Q在?Java ?managed C# {虚拟机上执行的Q来~写应用E序避免了许多潜在问题。即使用 C ?C++ 来编写代码,q是需要用一个可靠的垃圾攉库。在 2006 q_台式机程序员或服务器E序员不应该q需要管理内存?/p> Java q行时对其自w的代码起到了额外保护层的作用。在一?.class 文g装蝲到虚拟机之前Q该文g要由一个字节符验证器或一个可选的 SecurityManager q行验证。Java q不假设创徏 .class 文g的编译器没有 bug 且运转正常。设?Java 语言之初是Z允许在一个安全沙׃q行不信ȝ、潜在恶意的代码。它甚至不信d自n~译q的代码。毕竟,也许有h已经用十六进制编辑器手工修改了字节符Q试图触发缓冲器溢出。我们大安应该Ҏ们的E序也有对输入这L偏执?/p> 以敌人的角度思?/strong> 之前介绍的每Ҏ术都在阻止意外破坏方面造诣颇深。将它们l合h恰当地实玎ͼ会将未被发现的非蓄意破坏发生的可能性几乎减到零。(当然Qƈ不会减少到零Q但其发生的可能性就如同一束偏轨道的宇宙线您 CPU q算 1+1 的结果变?3 的可能性一样微乎其微。)但不是所有的数据损坏都是非蓄意的。如果有人故意引入坏数据来破坏程序的安全性又该如何呢Q以一个攻击者的角度q行思考是防护代码的下一个步骤?/p> 转回C个攻击者的角度q行思考,假设要攻ȝ应用E序是用 Java ~程语言~写的、用非本地代码且将所有额外数据都?XMLQ在接受前经q彻底验证)形式存储Q还能成功攻dQ是的,能。但用随机改变文件字节的低Ҏ昄不行。需要一U更为复杂的Ҏ来说明程序自w的错误机制及路径?/p> 当测试一个抵御模p攻ȝ应用E序Ӟ不可能做U黑盒测试,但通过一些明昄修改Q基本的xq是可以应用的。例如,考虑校验和,如果文g格式包含一个校验和Q在文件传臛_用程序前仅仅修改此校验和可以其同随机数据相匹配?/p> 对于 XMLQ试着模糊单独的元素内容和属性|而不是从文档中挑选一部分随机的字节进行替换。一定要用合法的 XML 字符替换数据Q而不要用随机字节Q因为即使一癑֭节的随机数据也一定是畸Ş的。也可以改变元素名称和属性名Uͼ只要l心地确保得到的文档格式仍是正确的就可以了。如果该 XML 文档是由一个限刉怸格的模式q行查的Q还需要计出该模式没?查什么,以决定在哪里q行有效的模p?/p> 一个结合了对剩余数据进行代码验证的真正严格的模式也许不会留下可操U늚I间。这是作ؓ一个开发h员所需要追求的。应用程序应能够处理所发送的M有意义的字节,而不会因权利上( de jure Q?无效而拒l?/p> l束?/strong> 模糊试能够说明 bug 在程序中的出现。ƈ不证明不存在q样?bug。而且Q通过模糊试会极大地提高您对应用E序的健壮性及抵M意外输入的安全性的自信心。如果您?24 时对程序进行模p测试而其依然无事Q那么随后同U类型的d׃大可能再危及到它。(q不是不可能Q提醒您Q只是可能性很。)如果模糊试揭示出程序中?bugQ就应该q行修正Q而不是当 bug 随机出现时再对付它们。模p测试通过明智C用校验和、XML、垃圾收集和/或基于语法的文g格式Q更有效CҎ上加Z文g格式?/p> 模糊试是一用于验证程序中真实错误的重要工P也是所有意识到安全性问题且着力于E序健壮性的E序员们的工L中所必备的工兗?/p> 关于作?/strong> Elliotte Rusty Harold 来自新奥良Q?现在他还定期回老家喝一美味的U葵汤。不q目前,他和d Beth 定居在纽U͘q布鲁克林的 Prospect HeightsQ同住的q有他的猫咪 CharmQ取自夸克)?MarjorieQ取自他x的名字)。他?Polytechnic 大学计算机科学的副教授,他在该校讲授 Java 和面向对象编E。他?Web 站点 Cafe au Lait 已经成ؓ Internet 上最行的独?Java 站点之一Q它的姊妹站?Cafe con Leche 已经成ؓ最行?XML 站点之一。他的书包括 Effective XML?Processing XML with Java?Java Network Programming ?The XML 1.1 Bible。他目前在从事处?XML ?XOM API、Jaxen XPath 引擎?Jester 试覆盖率工L开发工作?/p>试Z C 的应用程?br />当字W串包含额外的零Ӟ许多?C ~写的程序都会出问题 —?q类问题太过频繁以至于额外的零能够彻底隐藏代码中其他的问题。一旦验证出E序存在零字节问题,可以移除它们,从而让其他的问题Q现出来?
清单 1. 用随机数据替换文仉分的c?br />import java.io.*;
import java.security.SecureRandom;
import java.util.Random;
public class Fuzzer {
private Random random = new SecureRandom();
private int count = 1;
public File fuzz(File in, int start, int length) throws IOException
{
byte[] data = new byte[(int) in.length()];
DataInputStream din = new DataInputStream(new FileInputStream(in));
din.readFully(data);
fuzz(data, start, length);
String name = "fuzz_" + count + "_" + in.getName();
File fout = new File(name);
FileOutputStream out = new FileOutputStream(fout);
out.write(data);
out.close();
din.close();
count++;
return fout;
}
// Modifies byte array in place
public void fuzz(byte[] in, int start, int length) {
byte[] fuzz = new byte[length];
random.nextBytes(fuzz);
System.arraycopy(fuzz, 0, in, start, fuzz.length);
}
}
关于代码
我可以用很多U方式优?清单 1 中的代码。例如,有着 java.nio 的内存映文件是一个相当不错的选择。我也能够改q这个错误处理及可配|性。因Z惌q些l节hq里所要说明的观点Q所以我代码保持了原样?不切实际的限?br />如果真想要破坏一?XML 解析器,有几U方法可以试试。例如,大多?XML 解析器服从于特定的最大尺寸。如果一个元素名长度过 22 亿字W(Java String 的最大尺寸)QSAX 解析器将会失败。尽如此,在实践中q些极限值如此之高,以至于在辑ֈ之前内存已l耗尽?
]]>
试驱动开?Test Driven Development/TDD
试用例/TestCase/TC
设计/Design
重构/Refactoring
{TDD的目标}
Clean Code That Works
q句话的含义是,事实上我们只做两件事情:让代码奏效(WorkQ和让代码洁净QCleanQ,前者是把事情做对,后者是把事情做好。想想看Q其实我们^时所做的所有工作,除去无用的工作和错误的工作以外,真正正确的工作,q且是真正有意义的工作,其实也就只有两大c:增加功能和提升设计,而TDD 正是在这个原则上产生的。如果您的工作ƈ非我们想象的q样Q(q意味着您还存在W三cL有意义的工作,或者您所要做的根本和我们在说的是两回事)Q那么这告诉我们您ƈ不需要TDDQ或者不适用TDD。而如果我们偶然猜对(q对于我来说是偶Ӟ而对于Kent Beck和Martin Fowlerq样的大师来说则是辛勤工作的成果Q,那么恭喜您,TDD有可能成为您显著提升工作效率的一件法宝。请不要信疑Q若卌,因ؓM一Ҏ的技术——只要是从根本上改变人的行ؓ方式的技术——就必然使得怿它的来越怿Q不信的来越不信。这好比学游泳Q唯一能学会游泳的途径是亲自下去游,除此之外别无他法。这也好比成功学Q即使把卡耐基或希博士的书倒背如流也不能拥有积极的心态,可当你以U极的心态去成就了一番事业之后,你就再也M开它了。相信我QTDD也是q样Q想试用TDD的h们,请遵循下面的步骤Q?/p>
~写TestCase --> 实现TestCase --> 重构 Q确定范围和目标Q?/td> Q增加功能) Q提升设计)
[友情提示Q敏捷徏模中的一个相当重要的实践被称为:Prove it With CodeQ这U想法和TDD不谋而合。]
{TDD的优点}
『充满吸引力的优炏V?/b>
『不显而易见的优点?/b>
『有争议的优炏V?/b>
{TDD的步骤}
~写TestCase --> 实现TestCase --> 重构 Q不可运行) Q可q行Q?/td> Q重构)
步骤 | 制品 |
Q?Q快速新增一个测试用?/td> | 新的TestCase |
Q?Q编译所有代码,刚刚写的那个试很可能编译不通过 | 原始的TODO List |
Q?Q做可能少的改动,让编译通过 | Interface |
Q?Q运行所有的试Q发现最新的试不能~译通过 | Q?Red Bar) |
Q?Q做可能少的改动,让测试通过 | Implementation |
Q?Q运行所有的试Q保证每个都能通过 | Q?Green Bar) |
Q?Q重构代码,以消除重复设?/td> | Clean Code That Works |
{FAQ}
[什么时候重构?]
如果您在软g公司工作Q就意味着您成天都会和想通过重构改善代码质量的想法打交道Q不仅您如此Q您的大部分同事也都如此。可是,I竟什么时候该重构Q什么情况下应该重构呢?我相信您和您的同事可能有很多不同的看法,最常见的答案是“该重构旉构”,“写不下ȝ时候重构”,和“下一ơP代开始之前重构”,或者干脆就是“最q没旉Q就不重构了Q下ơ有旉的时候重构吧”。正如您已经预见到我惌的——这些想法都是对重构的误解。重构不是一U构Y件的工具Q不是一U设计Y件的模式Q也不是一个Y件开发过E中的环节,正确理解重构的h应该把重构看成一U书写代码的方式Q或习惯Q重构时时刻L可能发生。在TDD中,除去~写试用例和实现测试用例之外的所有工作都是重构,所以,没有重构M设计都不能实现。至于什么时候重构嘛Q还要分开看,有三句话是我的经验:实现试用例旉构代码,完成某个Ҏ时重构设计Q品的重构完成后还要记得重构一下测试用例哦?/p>
[什么时候设计?]
q个问题比前面一个要隑֛{的多,实话实说Q本人在依照TDD开发Y件的时候也常常被这个问题困扎ͼL觉得有些问题应该在写试用例之前定下来,而有些问题应该在新增一个一个测试用例的q程中自然出玎ͼ水到渠成。所以,我的是,设计的时机应该由开发者自己把握,不要受到TDD方式的限Ӟ但是Q不需要事先确定的事一定不能事先确定,免得捆住了自q手脚?/p>
[什么时候增加新的TestCaseQ]
没事做的时候。通常我们认ؓQ如果你要增加一个新的功能,那么先写一个不能通过?TestCaseQ如果你发现了一个bugQ那么先写一个不能通过的TestCaseQ如果你现在什么都没有Q从0开始,请先写一个不能通过?TestCase。所有的工作都是从一个TestCase开始。此外,q要注意的是Q一些大师要求我们每ơ只允许有一个TestCase亮红灯,在这?TestCase没有Green之前不可以写别的TestCaseQ这U要求可以适当考虑Q但即有多个TestCase亮红灯也不要紧,q未q反TDD 的主要精?/p>
[TestCase该怎么写?]
试用例的编写实际上是两个q程Q用尚不存在的代码和定义这些代码的执行l果。所以一?TestCase也就应该包括两个部分——场景和断言。第一ơ写TestCase的h会有很大的不适应的感觉,因ؓ你之前所写的所有东襉K是在解决问题Q现在要你提出问题确实不大习惯,不过不用担心Q你正在做正的事情Q而这个世界上最隄事情也不在于如何解决问题Q而在于ask the right questionQ?/p>
[TDD能帮助我消除Bug吗?]
{:不能Q千万不要把“测试”和“除虫”Z谈!“除虫”是指程序员通过自己的努力来减少bug的数量(消除bugq样的字眼我们还是不要讲为好^_^Q,而“测试”是指程序员书写产品以外的一D代码来保产品能有效工作。虽然TDD所~写的测试用例在一定程度上为寻找bug提供了依据,但事实上Q按照TDD的方式进行的软g开发是不可能通过TDD再找到bug的(x我们前面说的“完工时完工”)Q你惛_Q当我们的代码完成的时候,所有的试用例都亮了绿灯,q时隐藏在代码中的bug一个都不会露出马脚来?/p>
但是Q如果要问“测试”和“除虫”之间有什么联p,我相信还是有很多话可以讲的,比如TDD事实上减了bug的数量,把查找bug战役的关注点从全U战场提升到代码战场以上。还有,bug的最可怕之处不在于隐藏之深Q而在于满天遍野。如果你发现了一个用户很不容易才能发现的bugQ那么不一定对工作做出了什么杰A献,但是如果你发CD代码中Qbug的密度或LE度q高Q那么恭喜你Q你应该抛弃q写这D代码了。TDD避免了这U情况,所以将Lbug的工作降低到了一个新的低度?/p>
[我该Z个Feature~写TestCaseq是Z个类~写TestCaseQ]
初学者常问的问题。虽然我们从TDD 的说明书上看到应该ؓ一个特性编写相应的TestCaseQ但Z么著名的TDD大师所写的TestCase都是和类/Ҏ一一对应的呢Qؓ了解释这个问题,我和我的同事们都做了很多试验Q最后我们得C一个结论,虽然我不知道是否正确Q但是如果您没有{案Q可以姑且相信我们?/p>
我们的研I结果表明,通常在一个特性的开发开始时Q我们针对特性编写测试用例,如果您发现这个特性无法用TestCase表达Q那么请这个特性细分,直至您可以ؓ手上的特性写出TestCase为止。从q里开始是最安全的,它不会导致Q何设计上重大的失误。但是,随着您不断的重构代码Q不断的重构 TestCaseQ不断的依据TDD的思想做下去,最后当产品伴随试用例集一起发布的时候,您就会不l意的发现经q重构以后的试用例很可能是和品中的类/Ҏ一一对应的?/p>
[什么时候应该将全部试都运行一遍?]
Good QuestionQ大师们要求我们每次重构之后都要完整的运行一遍测试用例。这个要求可以理解,因ؓ重构很可能会改变整个代码的结构或设计Q从而导致不可预见的后果Q但是如果我正在开发的是一个ERP怎么办?q行一遍完整的试用例可能花Ҏ个小Ӟ况且现在很多重构都是由工具做到的Q这个要求的可行性和前提条g都有所动摇。所以我认ؓ原则上你可以挑几个你觉得可能受到本次重构影响的TestCase去runQ但是如果运行整个测试包只要p数秒的时_那么不介意你按大师的要求d?/p>
[什么时候改q一个TestCaseQ]
增加的测试用例或重构以后的代码导致了原来的TestCase的失M效果Q变得无意义Q甚臛_能导致错误的l果Q这时是改进TestCase的最好时机。但是有时你会发玎ͼq样做仅仅导致了原来的TestCase在设计上是臃肿的Q或者是冗余的,q都不要紧,只要它没有失效,你仍然不用去改进它。记住,TestCase不是你的产品Q它不要好看Q也不要怎么太科学,甚至没有性能要求Q它只要能完成它的命就可以了——这也证明了我们后面所说的“用Ctrl-C/Ctrl-V~写试用例”的可行性?/p>
但是Q美国h的想法其实跟我们q是不太一P拿托巴赞的MindMap来说吧,其实画MindMap只是Z表现自己的思\Q或记忆某些重要的事情,但托却大家把MindMapL一件艺术品Q甚臌有很多艺术家把自q的抽象派MindMap拿出来帮助托做宣传。同P大师们也要求我们把TestCase写的跟代码一栯量精良,可我惌的是Q现在国内有几个公司能把产品的代码写的精良?Q还是一步一步慢慢来吧?/p>
[Z么原来通过的测试用例现在不能通过了?]
q是一个警报,Red AlertQ它可能表达了两层意思——都不是什么好意思—?Q你刚刚q行的重构可能失败了Q或存在一些错误未被发玎ͼ臛_重构的结果和原来的代码不{h了?Q你刚刚增加的TestCase所表达的意思跟前面已经有的TestCase相冲H,也就是说Q新增的功能q背了已有的设计Q这U情况大部分可能是之前的设计错了。但无论哪错了,无论是那层意思,x到这个问题的Ҏ都比TDD的正常工作要难?/p>
[我怎么知道那里该有一个方法还是该有一个类Q]
q个问题也是常常出现在我的脑中Q无Z是第一ơ接触TDD或者已l成?TDD专家Q这个问题都会缠l着你不放。不q问题的{案可以参考前面的“什么时候设计”一节,{案不是唯一的。其实多数时候你不必考虑未来Q今天只做今天的事,只要有重构工P从方法到cd从类到方法都很容易?/p>
[我要写一个TestCaseQ可是不知道从哪里开始?]
从最重要的事开始,what matters mostQ从脚下开始,从手头上的工作开始,从眼前的事开始。从一个没有UI的核心特性开始,从算法开始,或者从最有可能耽误旉的模块开始,从一个最严重的bug开始。这是TDDM者和鼠目寸光者的一个共同点Q不同点是前者早已成竹在胸?/p>
[Z么我的测试L看v来有Ҏ蠢?]
哦?是吗Q来Q握个手Q我的也是!不必担心q一点,事实上,大师们给的例子也相当愚蠢Q比如一个极端的例子是要写一个两个int变量相加的方法,大师先断a2+3=5Q再断言5+5=10Q难道这些代码不是很愚蠢吗?其实q只是一个极端的例子Q当你初ơ接触TDDӞ写这L代码没什么不好,以后当你熟练时就会发现这样写没必要了Q要CQ谦虚是通往TDD的必l之路!从经典开发方法{向TDD像从面向过E{向面向对象一样困难,你可能什么都懂,但你写出来的cL有一个纯OO的!我的同事q告诉我真正的太极拳Q其速度是很快的Q不比Q何一个快拌慢,但是初学者(通常是指学习太极拳的?0q_太不Ҏ把每个姿劉K做对Q所以只能慢慢来?/p>
[什么场合不适用TDDQ]
问的好,实有很多场合不适合使用TDD。比如对软g质量要求极高的军事或U研产品——神州六P人命兛_的Y件——医疗设备,{等Q再比如设计很重要必L前做好的软gQ这些都不适合TDDQ但是不适合TDD不代表不能写TestCaseQ只是作用不同,C不同|了?/p>
{Best Practise}
[微笑面对~译错误]
学生时代最x的是~译错误Q编译错误可能会被老师视ؓ上课不认真听评证据Q或者同学间怺嘲笑的砝码。甚至离开学校很多q的老程序员依然x它像x迟CP潜意识里g~译错误极有可能和工资挂钩(或者和智商挂钩Q反正都不是什么好事)。其实,只要提交到版本管理的代码没有~译错误可以了Q不要担心自己手上的代码的编译错误,通常Q编译错误都集中在下面三个方面:
Q?Q你的代码存在低U错?br />Q?Q由于某些Interface的实现尚不存在,所以被试代码无法~译
Q?Q由于某些代码尚不存在,所以测试代码无法编?br />h意第二点与第三点完全不同Q前者表明设计已存在Q而实C存在D的编译错误;后者则指仅有TestCase而其它什么都没有的情况,设计和实现都不存在,没有Interface也没有Implementation?/p>
另外Q编译器q有一个优点,那就是以最敏捷的n手告诉你Q你的代码中有那些错误。当然如果你拥有Eclipseq样可以及时提示~译错误的IDEQ就不需要这L功能了?/p>
[重视你的计划清单]
在非TDD的情况下Q尤其是传统的瀑布模型的情况下Q程序员不会不知道该做什么,事实上,L有设计或者别的什么制品在引导E序员开发。但是在TDD的情况下Q这U优势没有了Q所以一个计划清单对你来说十分重要,因ؓ你必自己发现该做什么。不同性格的h对于q一点会有不同的反应Q我怿qx做事没什么计划要依靠别h安排的hQ所谓将才)可能略有不适应Q不q不要紧QTasks和CalendarQ又U效率手册)早已成ؓC上班族的必备工具了;而^时工作生zd很有计划性的人,比如?)Q就会更喜欢q种自己可以掌控Plan的方式了?/p>
[废黜每日代码质量查]
如果我没有记错的话,PSP对于个h代码查的要求是蛮严格的,而同h在针对个人的问题上, TDD却徏议你废黜每日代码质量查,别v疑心Q因ZL在做TestCase要求你做的事情,q且L有办法(自动的)查代码有没有做到q些事情 ——红灯停l灯行,所以每日代码检查的旉可能被节省,对于一个严格的PSP实践者来_q个成本q是很可观的Q?/p>
此外Q对于每日代码质量检查的另一个好处,是帮助你认识自q代码Q全面的从宏观、微观、各个角度审视自q成果Q现在,当你依照TDD做事Ӟq个优点也不需要了Q还记得前面说的TDD的第二个优点吗,因ؓ你已l全面的使用了一遍你的代码,q完全可以达到目的?/p>
但是Q问题往往也ƈ不那么简单,现在有没有h能告诉我Q我如何全面审视我所写的试用例呢?别忘了,它们也是以代码的形式存在的哦。呵呵,但愿q个问题没有把你吓到Q因为我怿到目前ؓ止,它还不是瓉问题Q况且在~写产品代码的时候你q是会自ȝ发现很多试代码上的没考虑到的地方Q可以就此修改一下。道理就是如此,世界上没有Q何方法能代替你思考的q程Q所以也没有MҎ能阻止你犯错误,TDD仅能让你更容易发现这些错误而已?/p>
[如果无法完成一个大的测试,׃最的开始]
如果我无法开始怎么办,教科书上有个很好的例子:我要写一个电影列表的c,我不知道如何下手Q如何写试用例Q不要紧Q首先想象静态的l果Q如果我的电影列表刚刚徏立呢Q那么它应该是空的,OKQ就写这个断a吧,断言一个刚刚初始化的电影列表是I的。这不是愚蠢Q这是细节,奥运会五全能的金牌得主玛丽莲·金是这栯的:“成功h士的共同点在于……如果目标不够清晎ͼ他们会首先做通往成功道\上的每一个细步骤……”?/p>
[试~写自己的xUnit]
Kent Beck大家每当接触一个新的语a或开发^台的时候,p己写q个语言或^台的xUnitQ其实几乎所有常用的语言和^台都已经有了自己?xUnitQ而且都是大同异Q但是ؓ什么大师给Zq样的徏议呢。其实Kent Beck的意思是说通过q样的方式你可以很快的了解这个语a或^台的Ҏ,而且xUnit实很简单,只要知道原理很快p写出来。这对于那些喜欢自己写底层代码的人,或者喜Ƣ控制力的h而言是个好消息?/p>
[善于使用Ctrl-C/Ctrl-V来编写TestCase]
不必担心TestCase会有代码冗余的问题,让它冗余好了?/p>
[永远都是功能FirstQ改q可以稍后进行]
上面q个标题q可以改成另外一句话Q避免过渡设计!
[淘汰陈旧的用例]
舍不得孩子套不着狹{不要可惜陈旧的用例Q因为它们可能从概念上已l是错误的了Q或仅仅会得出错误的l果Q或者在某次重构之后失去了意义。当然也不一定非要删除它们,从TestSuite中除去(JUnitQ或加上IgnoredQNUnitQ标{也是一个好办法?/p>
[用TestCase做试验]
如果你在开始某个特性或产品的开发之前对某个领域不太熟悉或一无所知,或者对自己在该领域里的能力一无所知,那么你一定会选择做试验,在有单元试作工L情况下,你用TestCase做试验,q看h像你在写一个验证功能是否实现的 TestCase一P而事实上也一P只不q你所验证的不是代码本w,而是q些代码所依赖的环境?/p>
[TestCase之间应该量独立]
保证单独q行一个TestCase是有意义的?/p>
[不仅试必须要通过的代码,q要试必须不能通过的代码]
q是一个小技巧,也是不同于设计思\的东ѝ像界的值或者ؕ码,或者类型不W的变量Q这些输入都可能会导致某个异常的抛出Q或者导致一个标C“illegal parameters”的q回|q两U情况你都应该测试。当然我们无法枚举所有错误的输入或外部环境,q就像我们无法枚举所有正的输入和外部环境一P只要TestCase能说明问题就可以了?/p>
[~写代码的第一步,是在TestCase中用Ctrl-C]
q是一个高U技巧,呃,是的Q我是这个意思,我不是说q个技巧难以掌握,而是说这个技巧当且仅当你已经是一个TDD高手Ӟ你才能体会到它的力。多ơ用TDD的h都有q样的体会,既然我的TestCase已经写的很好了,很能说明问题Qؓ什么我的代码不能从TestCase拯一些东西来呢。当Ӟq要求你的TestCase已经h很好的表达能力,比如断言f (5)=125的方式显然没有断af(5)=5^(5-2)表达更多的内宏V?/p>
[试用例包应该尽量设计成可以自动q行的]
如果产品是需要交付源代码的,那我们应该允许用户对代码q行修改或扩充后在自q环境下run整个试用例包。既焉常情况下的产品是可以自动运行的Q那Z么同样作Z付用L制品Q测试用例包׃是自动运行的呢?即产品不需要交付源代码Q测试用例包也应该设计成可以自动q行的,qؓ试部门或下一版本的开发h员提供了极大的便利?/p>
[只亮一盏红灯]
大师的徏议,前面已经提到了,仅仅是徏议?/p>
[用TestCase描述你发现的bug]
如果你在另一个部门的同事使用了你的代码,q且Q他发现了一个bugQ你猜他会怎么做?他会立即走到你的工位边上Q大声斥责说Q“你有bugQ”吗Q如果他胆敢q样对你Q对不vQ你一定要冷静下来Q不要当面回骂他Q相反你可以微微一W,然后心^气和的对他说Q“哦Q是吗?那么好吧Q给我一个TestCase证明一下。”现在局势已l倒向你这一边了Q如果他q没有准备好回答你这致命的一击,我猜他会感到非常愧Qƈ在内心责怪自己太莽撞。事实上Q如果他的TestCase没有q多的要求你的代码(而是按你们事前的契约Q,q且亮了U灯Q那么就可以定是你的bugQ反之,Ҏ则无理了。用TestCase描述bug的另一个好处是Q不会因Z后的修改而再ơ暴露这个bugQ它已经成ؓ你发布每一个版本之前所必须查的内容了?/p>
{关于单元试}
单元试的目标是
Keep the bar green to keep the code clean
q句话的含义是,事实上我们只做两件事情:让代码奏效(Keep the bar greenQ和让代码洁净QKeep the code cleanQ,前者是把事情做对,后者是把事情做好,两者既是TDD中的两顶帽子Q又是xUnit架构中的因果关系?/p>
单元试作ؓ软g试的一个类别,q是xUnit架构创造的Q而是很早有了。但是xUnit架构使得单元试变得直接、简单、高效和规范Q这也是单元试最q几q飞速发展成量一个开发工具和环境的主要指标之一的原因。正如Martin Fowler所_“Y件工E有史以来从没有如此众多的h大大收益于如此简单的代码Q”而且多数语言和^台的xUnit架构都是大同异Q有的仅是语a不同Q其中最有代表性的是JUnit和NUnitQ后者是前者的创新和扩展。一个单元测试框架xUnit应该Q?Q每个TestCase独立q行Q?Q每个TestCase可以独立和报告错误Q?Q易于在每次q行之前选择TestCase。下面是我枚丑և的xUnit框架的概念,q些概念构成了当前业界单元测试理论和工具的核心:
[试Ҏ/TestMethod]
试的最单位,直接表示Z码?/p>
[试用例/TestCase]
由多个测试方法组成,是一个完整的对象Q是很多TestRunner执行的最单位?/p>
[试容器/TestSuite]
由多个测试用例构成,意在把相同含义的试用例手动安排在一PTestSuite可以呈树状结构因而便于管理。在实现ӞTestSuite形式上往往也是一个TestCase或TestFixture?/p>
[断言/Assertion]
断言一般有三类Q分别是比较断言Q如assertEqualsQ,条g断言Q如isTrueQ,和断a工具Q如failQ?/p>
[试讑֤/TestFixture]
为每个测试用例安排一个SetUpҎ和一个TearDownҎQ前者用于在执行该测试用例或该用例中的每个测试方法前调用以初始化某些内容Q后者在执行该测试用例或该用例中的每个方法之后调用,通常用来消除试对系l所做的修改?/p>
[期望异常/Expected Exception]
期望该测试方法抛出某U指定的异常Q作Z个“断a”内容,同时也防止因为合情合理的异常而意外的l止了测试过E?/p>
[U类/Category]
为测试用例分c,实际使用时一般有TestSuite׃再用CategoryQ有Category׃再用TestSuite?/p>
[忽略/Ignored]
讑֮该测试用例或试Ҏ被忽略,也就是不执行的意思。有些被抛弃的TestCase不愿删除Q可以定为Ignored?/p>
[试执行?TestRunner]
执行试的工P表示以何U方式执行测试,别误会,q可不是在代码中规定的,完全是与试内容无关的行为。比如文本方式,AWT方式Qswing方式Q或者Eclipse的一个视囄{?/p>
{实例QFibonacci数列}
下面的Sample展示TDDer是如何编写一个旨在生Fibonacci数列的方法?br />Q?Q首先写一个TCQ断afib(1) = 1;fib(2) = 1;q表C数列的第一个元素和W二个元素都??/p>
Q?Q上面这D代码不能编译通过QGreatQ——是的,我是说GreatQ当Ӟ如果你正在用的是Eclipse那你不需要编译,Eclipse 会告诉你不存在fibҎQ单击mark会问你要不要新徏一个fibҎQOhQ当ӞZ让上面那个TC能通过Q我们这样写Q?/p>
Q?Q现在那个TC亮了l灯QwowQ应该庆一下了。接下来要增加TC的难度了Q测W三个元素?/p>
不过q样写还不太好看Q不如这样写Q?/p>
Q?Q新增加的断aD了红灯,Z扭{q一局势我们这样修改fibҎQ其中部分代码是从上面的代码中Ctrl-C/Ctrl-V来的Q?/p>
Q?Q天哪,q真是个׃h写的代码Q是啊,不是吗?因ؓTC是产品的蓝本,产品只要恰好满TCok。所以事情发展到q个地步不是fibҎ的错Q而是TC的错Q于是TCq要q一步要求:
Q?Q上有政{下有对{?/p>
Q?Q好了,不玩了。现在已l不是贱不贱的问题了Q现在的问题是代码出C冗余Q所以我们要做的是——重构:
Q?Q好Q现在你已经fibҎ已经写完了吗Q错了,一个危险的错误Q你忘了错误的输入了。我们o0表示Fibonacci中没有这一V?/p>
then change the method fib to make the bar greanQ?/p>
Q?Q下班前最后一件事情,把TC也重构一下:
Q?0Q打完收工?/p>
{关于本文的写作}
在本文的写作q程中,作者也用到了TDD的思维Q事实上作者先构思要写一什么样的文章,然后写出q篇文章应该满的几个要求,包括功能的要求(要写些什么)和性能的要求(可读性如何)和质量的要求Q文字的要求Q,q些要求起初是一个也达不到的Q因为正文还一个字没有Q,在这U情况下作者的文章无法~译通过Qؓ了达到这些要求,作者不停的写啊写啊Q终于在花尽了两个月的心血之后完成了当初既定的所有要求(make the bar greenQ,随后作者整理了一下文章的l构Q重构)Q在满意的提交给了Blogpȝ之后Q作者穿上了一件绿色的汗衫Q趴在地上,学了两声青蛙叫。。。。。。。^_^
{后记QMartin Fowler在中国}
从本文正式完成到发表的几个小旉Q我偶然d了Martin Fowler先生北京访谈录,光提到了很多对试驱动开发的看法Q摘抄在此:
Martin FowlerQ当Ӟ值得׃半的旉来写单元试Q!因ؓ单元试能够使你更快的完成工作。无数次的实践已l证明这一炏V你的时间越是紧张,p要写单元试Q它看上LQ但实际上能够帮助你更快、更舒服地达到目的?br />Martin FowlerQ什么叫重要Q什么叫不重要?q是需要逐渐认识的,不是惛_然的。我为绝大多数的模块写单元测试,是有点烦人,但是当你意识到这工作的h值时Q你会欣然的?br />Martin FowlerQ对全世界的E序员我都是那么几条Q……第二,学习试驱动开发,q种新的Ҏ会改变你对于软g开发的看法。…?/font>
——《程序员》,2005q?月刊
{鸣谢}
fhawk
Dennis Chen
般若菩提
Kent Beck
Martin Fowler
c2.com
Q{载本文需注明出处QBrian Sun @ 爬树的泡[http://m.tkk7.com/briansun]Q?/p>