测试驱动开发全功略
{关键字} 测试驱动开发/Test Driven Development/TDD {TDD的目标}
这句话的含义是,事实上我们只做两件事情:让代码奏效(Work)和让代码洁净(Clean),前者是把事情做对,后者是把事情做好。想想看,其实我们平时所做的所有工作,除去无用的工作和错误的工作以外,真正正确的工作,并且是真正有意义的工作,其实也就只有两大类:增加功能和提升设计,而TDD正是在这个原则上产生的。如果您的工作并非我们想象的这样,(这意味着您还存在第三类正确有意义的工作,或者您所要做的根本和我们在说的是两回事),那么这告诉我们您并不需要TDD,或者不适用TDD。而如果我们偶然猜对(这对于我来说是偶然,而对于Kent Beck和Martin Fowler这样的大师来说则是辛勤工作的成果),那么恭喜您,TDD有可能成为您显著提升工作效率的一件法宝。请不要将信将疑,若即若离,因为任何一项新的技术——只要是从根本上改变人的行为方式的技术——就必然使得相信它的人越来越相信,不信的人越来越不信。这就好比学游泳,唯一能学会游泳的途径就是亲自下去游,除此之外别无他法。这也好比成功学,即使把卡耐基或希尔博士的书倒背如流也不能拥有积极的心态,可当你以积极的心态去成就了一番事业之后,你就再也离不开它了。相信我,TDD也是这样!想试用TDD的人们,请遵循下面的步骤:
[友情提示:敏捷建模中的一个相当重要的实践被称为:Prove it With Code,这种想法和TDD不谋而合。] {TDD的优点}
『充满吸引力的优点』
{FAQ} [什么时候重构?] [什么时候设计?] [什么时候增加新的TestCase?] [TestCase该怎么写?] [TDD能帮助我消除Bug吗?] 但是,如果要问“测试”和“除虫”之间有什么联系,我相信还是有很多话可以讲的,比如TDD事实上减少了bug的数量,把查找bug战役的关注点从全线战场提升到代码战场以上。还有,bug的最可怕之处不在于隐藏之深,而在于满天遍野。如果你发现了一个用户很不容易才能发现的bug,那么不一定对工作做出了什么杰出贡献,但是如果你发现一段代码中,bug的密度或离散程度过高,那么恭喜你,你应该抛弃并重写这段代码了。TDD避免了这种情况,所以将寻找bug的工作降低到了一个新的低度。 [我该为一个Feature编写TestCase还是为一个类编写TestCase?] 我们的研究结果表明,通常在一个特性的开发开始时,我们针对特性编写测试用例,如果您发现这个特性无法用TestCase表达,那么请将这个特性细分,直至您可以为手上的特性写出TestCase为止。从这里开始是最安全的,它不会导致任何设计上重大的失误。但是,随着您不断的重构代码,不断的重构TestCase,不断的依据TDD的思想做下去,最后当产品伴随测试用例集一起发布的时候,您就会不经意的发现经过重构以后的测试用例很可能是和产品中的类/方法一一对应的。 [什么时候应该将全部测试都运行一遍?] [什么时候改进一个TestCase?] 但是,美国人的想法其实跟我们还是不太一样,拿托尼巴赞的MindMap来说吧,其实画MindMap只是为了表现自己的思路,或记忆某些重要的事情,但托尼却建议大家把MindMap画成一件艺术品,甚至还有很多艺术家把自己画的抽象派MindMap拿出来帮助托尼做宣传。同样,大师们也要求我们把TestCase写的跟代码一样质量精良,可我想说的是,现在国内有几个公司能把产品的代码写的精良??还是一步一步慢慢来吧。 [为什么原来通过的测试用例现在不能通过了?] [我怎么知道那里该有一个方法还是该有一个类?] [我要写一个TestCase,可是不知道从哪里开始?] [为什么我的测试总是看起来有点愚蠢?] [什么场合不适用TDD?] {Best Practise} [微笑面对编译错误] 另外,编译器还有一个优点,那就是以最敏捷的身手告诉你,你的代码中有那些错误。当然如果你拥有Eclipse这样可以及时提示编译错误的IDE,就不需要这样的功能了。 [重视你的计划清单] [废黜每日代码质量检查] 此外,对于每日代码质量检查的另一个好处,就是帮助你认识自己的代码,全面的从宏观、微观、各个角度审视自己的成果,现在,当你依照TDD做事时,这个优点也不需要了,还记得前面说的TDD的第二个优点吗,因为你已经全面的使用了一遍你的代码,这完全可以达到目的。 但是,问题往往也并不那么简单,现在有没有人能告诉我,我如何全面审视我所写的测试用例呢?别忘了,它们也是以代码的形式存在的哦。呵呵,但愿这个问题没有把你吓到,因为我相信到目前为止,它还不是瓶颈问题,况且在编写产品代码的时候你还是会自主的发现很多测试代码上的没考虑到的地方,可以就此修改一下。道理就是如此,世界上没有任何方法能代替你思考的过程,所以也没有任何方法能阻止你犯错误,TDD仅能让你更容易发现这些错误而已。 [如果无法完成一个大的测试,就从最小的开始] [尝试编写自己的xUnit] [善于使用Ctrl-C/Ctrl-V来编写TestCase] [永远都是功能First,改进可以稍后进行] [淘汰陈旧的用例] [用TestCase做试验] [TestCase之间应该尽量独立] [不仅测试必须要通过的代码,还要测试必须不能通过的代码] [编写代码的第一步,是在TestCase中用Ctrl-C] [测试用例包应该尽量设计成可以自动运行的] [只亮一盏红灯] [用TestCase描述你发现的bug] {关于单元测试} 单元测试的目标是
这句话的含义是,事实上我们只做两件事情:让代码奏效(Keep the bar green)和让代码洁净(Keep the code clean),前者是把事情做对,后者是把事情做好,两者既是TDD中的两顶帽子,又是xUnit架构中的因果关系。 单元测试作为软件测试的一个类别,并非是xUnit架构创造的,而是很早就有了。但是xUnit架构使得单元测试变得直接、简单、高效和规范,这也是单元测试最近几年飞速发展成为衡量一个开发工具和环境的主要指标之一的原因。正如Martin Fowler所说:“软件工程有史以来从没有如此众多的人大大收益于如此简单的代码!”而且多数语言和平台的xUnit架构都是大同小异,有的仅是语言不同,其中最有代表性的是JUnit和NUnit,后者是前者的创新和扩展。一个单元测试框架xUnit应该:1)使每个TestCase独立运行;2)使每个TestCase可以独立检测和报告错误;3)易于在每次运行之前选择TestCase。下面是我枚举出的xUnit框架的概念,这些概念构成了当前业界单元测试理论和工具的核心: [测试方法/TestMethod] [测试用例/TestCase] [测试容器/TestSuite] [断言/Assertion] [测试设备/TestFixture] [期望异常/Expected Exception] [种类/Category] [忽略/Ignored] [测试执行器/TestRunner] {实例:Fibonacci数列} 下面的Sample展示TDDer是如何编写一个旨在产生Fibonacci数列的方法。
public
void
testFab(){
assertEquals( 1 ,fib( 1 )); assertEquals( 1 ,fib( 2 )); } (2)上面这段代码不能编译通过,Great!——是的,我是说Great!当然,如果你正在用的是Eclipse那你不需要编译,Eclipse会告诉你不存在fib方法,单击mark会问你要不要新建一个fib方法,Oh,当然!为了让上面那个TC能通过,我们这样写:
public
int
fib(
int
n){
return 1 ; } (3)现在那个TC亮了绿灯,wow!应该庆祝一下了。接下来要增加TC的难度了,测第三个元素。
public
void
testFab(){
assertEquals( 1 ,fib( 2 )); assertEquals( 2 ,fib( 3 )); } 不过这样写还不太好看,不如这样写:
public
void
testFab(){
assertEquals( 1 ,fib( 2 )); assertEquals(fib( 1 ) + fib( 2 ),fib( 3 )); } (4)新增加的断言导致了红灯,为了扭转这一局势我们这样修改fib方法,其中部分代码是从上面的代码中Ctrl-C/Ctrl-V来的:
public
int
fib(
int
n){
if (n == 3 ) return fib( 1 ) + fib( 2 ); return 1 ; } (5)天哪,这真是个贱人写的代码!是啊,不是吗?因为TC就是产品的蓝本,产品只要恰好满足TC就ok。所以事情发展到这个地步不是fib方法的错,而是TC的错,于是TC还要进一步要求:
public
void
testFab(){
assertEquals( 1 ,fib( 3 )); assertEquals(fib( 2 ) + fib( 3 ),fib( 4 )); } (6)上有政策下有对策。
public
int
fib(
int
n){
if (n == 3 ) return fib( 1 ) + fib( 2 ); if (n == 4 ) return fib( 2 ) + fib( 3 ); return 1 ; } (7)好了,不玩了。现在已经不是贱不贱的问题了,现在的问题是代码出现了冗余,所以我们要做的是——重构:
public
int
fib(
int
n){
if (n == 1 || n == 2 ) return 1 ; else return fib(n - 1 ) + fib(n - 2 ); } (8)好,现在你已经fib方法已经写完了吗?错了,一个危险的错误,你忘了错误的输入了。我们令0表示Fibonacci中没有这一项。
public
void
testFab(){
assertEquals( 1 ,fib( 4 )); assertEquals( 0 ,fib( 0 )); assertEquals( 0 ,fib( - 1 )); } then change the method fib to make the bar grean:
public
int
fib(
int
n){
if (n <= 0 ) return 0 ; if (n == 1 || n == 2 ) return 1 ; else return fib(n - 1 ) + fib(n - 2 ); } (9)下班前最后一件事情,把TC也重构一下:
public
void
testFab(){
int cases[][] = { { 0 , 0 },{ - 1 , // thewrongparameters { 1 , 1 },{ 2 , 1 }}; // thefirst2elements for ( int i = 0 ;i < cases.length;i ++ ) assertEquals(cases[i][ 1 ],fib(cases[i][ 0 ])); // therestelements for ( int i = 3 ;i < 20 ;i ++ ) assertEquals(fib(i - 1 ) + fib(i - 2 ),fib(i)); } (10)打完收工。 {关于本文的写作} 在本文的写作过程中,作者也用到了TDD的思维,事实上作者先构思要写一篇什么样的文章,然后写出这篇文章应该满足的几个要求,包括功能的要求(要写些什么)和性能的要求(可读性如何)和质量的要求(文字的要求),这些要求起初是一个也达不到的(因为正文还一个字没有),在这种情况下作者的文章无法编译通过,为了达到这些要求,作者不停的写啊写啊,终于在花尽了两个月的心血之后完成了当初既定的所有要求(make the bar green),随后作者整理了一下文章的结构(重构),在满意的提交给了Blog系统之后,作者穿上了一件绿色的汗衫,趴在地上,学了两声青蛙叫。。。。。。。^_^ (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |