SidelightofLife 2009-10-19
是的,现在肯定有读者会这样说了:“我只在产品发品之前写测试。”有些人可能会窃笑,对质量保证部门说三道四。还有一些人作为项目经理可能会添油加醋地说:“我们可不会浪费时间写测试代码;我们还得写真正的代码呢。”那么,采用TDD到底是什么意思呢?
TDD产生于敏捷开发运动,特别是极限编程(extreme programming,XP),而且TDD正是XP的一个核心原则。推崇TDD的人认为,不应该完成开发之后再写测试,这通常只是“马后炮”,而应当在写代码之前先写测试。测试本质上相当于设计文档,而不是花大量时间去摆弄一个复杂的图形化工具,你要直接在代码中“拟画”一个类。开始时先为一些小功能块编写测试。在有些语言下,你写的测试甚至不能编译,因为所引用的类尚不存在。一旦建立了测试,就可以运行这个测试(当然,此时运行测试会失败)。然后再编写最少量的代码,以便测试通过。接下来再重构代码,并增加更多的测试。
通常可以使用测试框架来帮助编写自动化测试。最著名的框架是JUnit,不过现在已经有很多xUnit项目,可以简化在各种语言下测试的创建。一般地,这些框架都建立在断言(assert)基础上。开发人员编写测试方法,将调用方法的实际结果与期望结果进行比较。当然,可以人工地检查一个日志文件或用户界面来完成这些比较,但是,用计算机完成数据比较不仅速度快,也更准确。另外,就算是让计算机把同样的测试运行上1500次,它们也不会嫌烦,如果是人可做不到这一点。
有了JUnit和其他测试框架,编写和运行测试变得相当简单。这就能鼓励开发人员创建大量测试(往往能更完备地覆盖各种测试情况),而且会乐于经常运行这些测试(可以更快地帮助开发人员找到bug)。在许多情况下,如果项目中采用了TDD,测试代码往往与生产代码一样多!
使用TDD可以带来许多重要的好处:
提供明确的目标:你很清楚,一旦结束(也就是一旦测试通过),你的工作就完成了(假设你的测试写得很好)。测试会为代码建立一个自然的边界,使你把重点集中在当前任务上。一旦测试通过,就有确切的证据证明你的代码能工作。相对于人工地测试用户界面或者比较日志文件中的结果,在一个xUnit框架中运行自动化测试,速度要快几个数量级。大多数xUnit测试的运行只需几微秒,而且大多数采用TDD的人都会一天运行数次测试。在许多开发小组中,将代码签入源代码树之前,代码必须成功地通过测试。
提供文档:你是不是经常遇到看不懂的代码?这些代码可能没有任何文档说明,而且开发代码的人可能早就走了(或者度假去了)。当然,看到这种代码的时间往往也很不合时宜,可能是凌晨3点,也可能有位副总在你旁边大声催促着赶快解决昨天的问题,这样要想花些时间来明白原作者的意图就更困难了。我们见过一些好的单元测试文档,它们会指出系统要做什么。测试就像原开发人员留下的记号,可以展示他们的类具体是怎么工作的。
改善设计:编写测试能改善设计。测试有助于你从界面的角度思考,测试框架也是代码的客户。测试能让你考虑得更简单。如果你确实遵循了“尽量简单而且行之有效”的原则,就不会写出篇幅达几页的复杂算法。要测试的代码通常依赖性更低,而且相互之间没有紧密的联系,因为这样测试起来更容易。当然,还有一个额外的作用,修改起来也会更容易!
鼓励重构:利用一套健壮的测试集,你能根据需要进行重构。你是不是经常遇到一些不知是否该修改的代码?种种顾虑让你行动迟缓,过于保守,因为你不能保证所做的修改会不会破坏系统。如果有一套好的单元测试集,就能放心地进行重构,同时能保证你的代码依然简洁。
提高速度:编写这么多测试会不会使开发速度减慢呢?人们经常会以速度(或开发开销)作为反对进行TDD和使用xUnit框架的理由。所有新的工具都会有学习曲线,但是一旦开发人员适应了他们选择的框架(通常只需很短的时间),开发速度实际上会加快。一个完备的单元测试集提供了一种方法对系统完成回归测试,这说明,增加一个新特性之后,你不必因为怀疑它会不会破坏原系统而寝食难安。
提供反馈:单元测试还有一个经常被忽略的优点,即开发的节奏。尽管看上去好像无关紧要,但通过测试之后你会有一种完成任务的成就感!你不会成天地修改代码而没有任何反馈,这种测试—代码—测试的方法会鼓励你动作幅度小一些,通常修改一次代码的时间仅几分钟而已。这样你不会一下子看到冒出一大堆的新特性,而只是让代码基一次前进一小步。
从我们的经验看,测试是会传染的,你可能会慢慢上瘾。一开始,许多开发人员都心存疑虑,但最终几乎每个开发人员都迷上了运行测试后的绿条。测试第一次“抓住”bug或者增加一个新特性,只需几分钟而不是几个小时,往往就是在这样一些时候,开发人员会欣喜地认识到测试确实很有意义。