[原文: Introduction to Test Driven Development (TDD)]
[中文名:测试驱动开发介绍(TDD)]
[出处: http://www.agiledata.org/essays/tdd.html]
[作者: Scott W. Ambler]
[翻译:极地银狐.NET]
测试驱动开发是一种先开发测试的先进技术,即你在编写足够的产品代码用于测试和重构之前就编写测试。测试驱动开发的初衷是什么呢?一种观点是TDD是一种规范而不是校验。就是说它是在你编码之前就惯穿设计的一种思考。另一个观点是TDD是一种编码技术,就像Ron Jeffries喜欢说的那样TDD可用来编写干净的可工作的代码。尽管我把决定权留给读者,但我想以上两点各有其道理。
目录
一、 什么是TDD?
二、 TDD与传统测试
三、 TDD与文档
四、 测试驱动数据库开发
五、 TDD与敏捷模型驱动开发(AMDD)
六、 为什么使用TDD?
七、 小结
八、 工具
一、 什么是TDD?
先行测试开发(Test First Development - TFD)的步骤已在图一的UML活动图中概括出来了。首先快速加入测试以及足以使测试失败的代码。然后运行测试,当然可能由于速度的原因你只决定运行整个测试的一小部分,用于确认新测试的失败。你修改你的功能代码使此次测试通过。第四步是再测试此代码,如果还是失败那你可能还要继续修改你的代码直到通过测试为止。一旦通过试就再回到第一步重新做起(可能你首先要重构设计中任何重复的代码,从TFD转到TDD)。
图一:先行测试开发步骤
我喜欢用以下简单的公式描述TDD:
TDD=TFD+重构
TDD和传统的测试是完全的两回事。你使用写功能代码之前编写测试代码这种方式来替代先写功能代码再追加测试代码。更多的,你以很小的步骤来做此事—一次只写一个测试代码和相应的功能代码。一个遵循TDD开发步骤的程序员会因为不是当前的功能代码而拒绝写新的直到有一个会失败的测试代码。实际上就算是多一行代码他们都是不愿意的,除非有新的测试。一旦有了测试代码他们才会做相应的开发来确认测试的通过(你的新功能和测试代码可能会扰乱现有的测试)。只要你的代码通过测试了,那就要重构它以确保代码质量。这在原则上听起来简单,但当你第一次用TDD开发步骤时它会证明(你)需要更多的训练,因为它很容易出现“脱节”而引起编写功能代码之前没有编写测试代码。结对编程(Williams and Kessler 2002)的一个优点就是你的搭档会帮助你跟踪。
TDD一个潜在的假设是你有一个可用的单元测试框架。尽管商业工具是一个可选的方案,但大多数敏捷开发人员还是愿意使用xUnit系列的开源工具,比如JUnit或VBUnit。没有这些工具TDD实际上是不可能的。图2是一个UML状态图,描述了人们是如何在典型的开发中使用xUnit工具的。这个图是Keith Ray.推荐给我的。
图2:通过xUnit框架进行测试
Kent Beck,一位致力于在极限编程(XP,Beck 2000)中普及TDD的人,他为TDD定义了两条简单的规则(Beck 2003)。一,在自动测试失败时只编写业务代码;二,消除任何你找到的任何重复(的代码或设计)。Beck解释了这两条规则是如何展现复杂的个体以及如何组织行为:
你进行有机的设计,运行的代码能根据结果产生回馈;
你自己编写测试,不可能因为人家为你写测试而一天等20次;
你的开发环境必须要求能对小的更改作出快速响应(比如你要能快速编译和进行回归测试);
你的设计必须高内聚低偶合以使测试更容易(比如你的设计高度规范化,这样能让你进步而且你的系统也会更容易维护)。
对程序员来说则暗示着应该学习如何进行有效的单元测试。Beck的经验认为好的单元测试:
运行快(它们有快速安装,执行和中断);
在隔离环境中运行(你可以重新部署它们);
使用数据以便容易阅读和理解;
在需要的时候使用真实数据(如产品数据的拷贝);
一步一步向你的目标前进;
二、 TDD与传统测试
TDD原本是一种编程技术,它间接地确保你的代码能彻底地被单元测试检查。可是,它能做的不只这些。你仍然需要参考传统的一些测试比如功能测试、用户验收测试、系统集成测试等等。大部分的这些测试都可以在项目开始之前就进行,如果你选择这么做的话(而且你也应该这么做)。实际在XP里,项目负责人指定的用户验收测试在编码时或是更早就开始进行了。告诉项目负责人系统能适应需求,增强他的信心!
一个好的传统测试能发现一个或多个漏洞,此理论同样适用于TDD。当一个测试失败的时候你必须进行改进因为你知道你需要解决这个问题。更重要的是,当你的测试不再失败的时候你就可以清楚地知道成功了多少。TDD增强了你的信心因为它适应了预先定下的需求,你的系统能正常运行了,因此你再自信地开发下去。
像传统的测试一样,项目风险越大,就要更彻底地进行测试。传统测试和TDD中你都不可能为着完美而奋斗,代之的是测试系统的重要性。为了解释敏捷模型(Agile Modeling, AM),你应该“带着目的去测试”、知道为什么要测试以及测试是在什么等级。TDD为你带来的有趣的间接影响是,你的代码测试覆盖率是100%—每一行都被测试过了—这是传统测试所不能保证的(虽然它们推荐这么做)。一般来讲我会很负责任地说TDD的结果比传统技术更有意义。
如果(为一个功能)值得写代码,那么一定值得测试。如果不值得测试,那又何必浪费时间去写一些无用的代码呢?
三、 TDD与文档
大部分的程序员都不喜欢读现有系统的文档,他们更喜欢看代码。这也无可厚非。当要了解一个类或是操作的时候他们一般是先去找相关的示例代码调用。书写良好的单元测试正是这么做的—它为你的功能代码提供了工作指引—作为结果的,单元测试有效地成为你技术文档有意义的一部分。这暗示着预期要用到的文档必须实事求是。
四、 测试驱动数据库开发
直到写这篇文章的时候,在敏捷开发团队中仍然在讨论一个问题:“TDD是否能用于面向数据的开发?”正如你在图1中所看到的处理流程那样,没有一个步骤是用某种特定的语言来描述的,比如JAVA或是C#,即使它们是TDD的典型使用环境。为什么不能在你修改数据库结构之前写一个测试?为什么你不做一些改变,运行测试,然后根据要求重构你的(数据库)结构?在我看来你只需要选择这样的工作方式就行。
我猜测在短期内数据库TDD没有一般应用程序的TDD好使。第一个挑战就是工具的支持。虽然单元测试工具,比如DBUnit,在写此文的时候现在已经表现出其可用的一面了。一些DBA正在提升他们测试的质量,但我没有看到哪个TDD接近了数据库开发的目标。另一个挑战是单元测试工具在数据库团队中并不能被很好的接受,虽然这正在被改变。所以我希望的是在以后的几年里数据库TDD成长起来。第二,新的开发概念对于很多数据库专家来说是比较新的,其次作为结果,使用TDD方式的动机一直悬而未决。这种情况会影响了数据库专家的工具的可用性—由于传统的思维在数据库团队里起支配作用而使得大多数工具不支持先进的开发方式。我希望工具提供者能和范例同步,但我希望的是我们用开源工具来代替。第三,我希望的是大多数在面向数据工作不喜欢测试驱动开发只喜欢模型驱动开发的人,我之所以这么说是因为直到目前测试驱动开发还没有被大范围的考虑。另一个理由是目前很多数据库专家都是喜欢虚拟设计,喜欢模型驱动开发。
五、 TDD与敏捷模型驱动开发(AMDD)
将TDD与模型驱动开发(MDD)相比较如何?或者是说和敏捷模型驱动开发(AMDD)呢?我相信:
TDD缩短了编程反馈周期而AMDD缩短了建模反馈周期;
TDD提供详细规范(测试)而AMDD提供一般规范(数据模型);
TDD有助于开发中编写高质量代码而AMDD有助于在项目中同项目负责人和开发人员进行有效地沟通;
TDD能对你开发的软件有一个具体形态的描述,AMDD能让你的团队,包括项目负责人,向着一个共有的目标前进;
TDD提供了具体文档的具体反馈而AMDD对具体文档允许口头反馈(具体反馈需要程序员在代码中证明,而那样就是非敏捷模型的技术了);
TDD可通过关注代码的可调用和可测试来看你的设计是否整洁,而AMDD提供了一个机会让你在写代码之前思考;
TDD是非可视化的而AMDD是可视化的;
两种技术对传统开发人员来说都是新的,搞不好会不爽它们;
两种技术都支持螺旋式开发;
你会采用哪种?答案取决于你,你的同伴,你自己的偏爱。一些人是“可视化思考者”,或是叫空间思考者,他们喜欢通过画东西来思考。另一些人偏向于组织文字,非可视化者或是非空间思考者,他们工作中不擅长画图,喜欢用TDD的方式。当然更多的人是在这两种极端之前,凭借感觉来适当地使用两种技术。简而言之,两种技术都使用以获取两种技术中的优点。
如何结合这两种技术?AMDD应用在和你的项目负责人一起建模以帮助浏览需求以及需求在架构和设计中的充分体现(通常是简单的草图)。TDD是用于确保你开发代码的整洁、可靠的重要组成部分。最后的结果是你有一个高质量、符合项目负责人要求的工作系统。
六、 为什么使用TDD?
TDD的一个好处是能让你小步小步地写软件。这是我几年来一直推行的方法因为它要比大步完成更具生产力。假设你有一些新的功能代码,编译和测试它们。当测试失败的时候你有很好的机会来排错。你能很快发现错误并修复它,这些错误可能是需要写两行代码,远远好过于你写2000行代码。同时也意味着你的编译和回归测试加快了。可见小步处理更具魅力。一般来说我喜欢在重新编译和测试之前加入新的几行代码,一般来说是10行以下。
我想Bob Martin说的对,“编写单元测试更像一种设计行为,文档行为而不是验证行为。编写单元测试缩短了反馈周期读数,最小读数基于功能验证。”(Martin, Newkirk, and Koss 2003)
敏捷技术给大多数人带来的第一误会是只适用于小项目,也许就是几个人花几个月时间的那种,不是那种“真正”的大项目。这明显错了。Beck (2003)报道了在Smalltalk下一个完全测试驱动开发的项目,40人年,有250000行功能代码和250000行测试代码。在20分钟里要跑4000个测试,一天中要进行好几次完全的测试。虽然算是大项目了,但我以前参加过更大的,几百人年的工作量(用TDD完成),由此可见TDD适用于任何大小的系统。
七、 小结
测试驱动开发(TDD)是一种在添加新代码之前让你的测试代码出错的一种开发技术。TDD很快被敏捷开发人员采用来进行应用系统源代码开发,很快也会将被敏捷DBA用于数据库开发。TDD可以作为敏捷模型驱动开发的补充方法(AMDD)而且两者可以一起使用。TDD不能替代传统的测试,相反它定义一种验证方式来确保单元测试的有效性。TDD的另一个作用是测试代码可以作为代码示例,因此可以为我们提供编码规范。在我的经验看来,TDD在实际开发中表现的难以相信的出色以至于很多敏捷开发者考虑采用之。
八、 工具
以下是一些典型的可用的TDD工具,如果有建议请给我写信。
[url]csUnit (.Net)[/url]
CUnit
DUnit (Delphi)
DBUnit
JUnit
OUnit
PHPUnit
PyUnit (Python)
NUnit
Test::Unit (Ruby)
VBUnit
Copyright 2002-2006 Scott W. Ambler
以下转自另外一个帖子
对TDD的解释
有一个blog胖子不减肥是我很喜欢看的,今天看到一段话,未经授权引用一下。
>>
UNIT TEST又叫单元测试,是针对程序最基本结构单元所进行的测试。而TDD的过程是这样的,写一个测试程序,使其可以运行,重构。在写这个测试程序的时候你考虑的不应该是基于什么结构单元,而是要考虑需要完成的什么功能。实现和重构的时候,具体是不是这个单元完成了这个功能依然不是你应该去考虑的,你考虑的还是——是不是完成了这个功能、是不是代码真的清晰和可工作。你考虑的问题永远是围绕着具体的功能进行的,而不是围绕某种结构进行的。你写这个测试程序的时候,这个结构并不存在,并且今后也可能不存在(由于重构,你在别的结构部分实现了这个功能)。
明白这个道理就可以明白TDD实际还是基于需求驱动的,还是一种前瞻性的设计手段。只不过TDD让这个需求更加具体,让其前瞻性也更可以预测,并且在多种方法中给了你进行多种尝试的机会。而当你认为这个测试只是单元测试的时候,无疑你就把程序的结构早早的做了一个固定,其是基于结构的而不是基于需求的,并且由于其基于结构的一面则设计的前瞻性很难得到保证,而就根本性的断绝了你进行多种尝试的可能。设计的前瞻性是指你的设计可以带来可以预测的结果。而软件的结构是动态的,并且随着你必须进行的重构活动这样的结构变更会日常性的存在。如果你的一个测试高度的依靠某种特殊的结构,在这样的经常性重构的环境下,其被经常性修改的几率会大大增加。而由于其结构的不确定性是根本不可能逆转的,所以针对结构进行的测试根本不可能带来结构上的可预测性,而谈不上什么前瞻性了。
<<
这段话对TDD作了清澈的分析,TDD不单纯是以测试来驱动代码的编写。
我们还没有实践TDD,不是每个成员都对TDD很认同,并且了解怎么来做,这包括我。但是有一点需要强调——TDD不单纯是以测试来驱动代码的编写,而是对整个开发流程的驱动。
其他的参考资料:
http://www.cnblogs.com/birdshome/archive/2006/06/24/thinking_drive_development.html