极限编程一 概述

极限编程诞生于一种加强开发者与用户的沟通需求,让客户全面参与软件的开发设计,保证变化的需求及时得到修正。要让客户能方便地与开发人员沟通,一定要用客户理解的语言,先测试再编码就是先给客户软件的外部轮廓,客户使用的功能展现,让客户感觉到未来软件的样子,先测试再编码与瀑布模型显然是背道而驰的。同时,极限编程注重用户反馈与让客户加入开发是一致的,让客户参与就是随时反馈软件是否符合客户的要求。有了反馈,开发子过程变短,迭代也就很自然出现了,快速迭代,小版本发布都让开发过程变成更多的自反馈过程,有些象更加细化的快速模型法。当然极限编程还加入了很多激励开发人员的“措施”,如结队编程、40小时工作等。

 

在XP的项目开发中,首先引入了

四个变量:

成本、时间、质量和范围

通过研究变量之间的相互作用,将项目开发分析的更加透彻,成功讲述一个项目成功的原则。

为了能成功地实施XP,XP制定了

四个准则(核心)

沟通、简单、反馈和勇气

十二条原则

计划游戏、小版本、隐喻、简单设计、测试、重构、结队编程、代码集体所有、持续集成、每周工作40小时、现场客户、编码标准

以及对开发人员的

工作要求

编码、测试、倾听和设计

提出这些要求和原则后,XP有提出了一系列的

解决方案(也就是策略)

其中包含:

管理策略、设施策略、计划策略、开发策略、设计策略和测试策略

在真正去实现XP时,XP又提供了

将策略成功应用的实践:

其中主要包括:

测试驱动开发、结队编程、重构和极限编程工具

 

 

四个核心:

极限编程的核心有四个,交流、简单、反馈和勇气,这四个原则大家在平时做项目的过程中一定也注意到了。但是两位大师Kent Beck 和 Martin Fowler能够把这四点归结在一起,使他们能够一起组成极限编程这架四轮马车,却是个不小的创造的。

下面仅就自己的学习和简单的实践过程中碰到的问题来谈谈自己对这四个核心的一些理解。

一、交流

1)       研发人员和客户的交流

这一点和传统的软件工程中有些类似,在平时研发软件的过程中也很注重和客户的交流,特别是在需求分析、概要设计连同验收测试的时候,研发人员和客户有效的交流是必不可少的,那将直接影响到一个项目是否能够符合客户的需要。

然而,在极限编程中客户所处的研发阶段有些不同,传统的项目研发过程中,客户只在最初的时候和最后的时候需要和研发人员在一起,他们的责任也就是在于业务功能上的帮助,但是这样就不可避免的导致了这样的一个状况:在项目最初的时候客户提出了错误的或不准确的需求,然后项目组开始研发,客户很长一段时间不介入项目,而在项目验收的时候发现有些地方有错误或需要修改,此时项目组不得不付出很多的时间和精力来适应客户的需求。这是时间和资金上很大的浪费。在极限编程中,需要一个很精通业务的现场客户,他们不但随时提供业务上的信息,而且要编写业务验收测试的测试代码,这样就能够在很大的程度上确保项目的方向不会错误。

极限编程的过程是“瞄准-》射击-》调整-》调整”的过程,并不强求在项目开始的时候就准确把握项目的方向,由于有现场客户的存在,项目的方向是不断的调整中的,这样就能够极大程度上避免项目走弯路。

2)       研发人员之间的交流

当前在招聘研发人员连同其他一切的工作人员的时候,我们都会强调团队精神,但是在实际的工作过程中,我们除了在出现问题,而且自己解决出现很大困难的时候才会去请教别人(我以前是这样的,可能每个人都会不同),再就是大家能够一起聚在一起闲聊、吃饭、唱歌等等研发过程以外的活动。以上的这些的确能够使团队之中产生一定的凝聚力,能够让大家和睦相处,但是离真正意义上的团队更有一定的差距。

我们所受到的教育一直培养的是一种单独解决问题的能力,所以,再碰到问题的时候我们想到的大多是自己来就解决,而不是和其他人一起来完成。

极限编程的实践中有一个很重要的原则就是结对编程,这个原则看起来似乎有些奇怪。因为我们第一个想到的问题就是让两个人来同时做一件事情,那么不就是浪费了一个人的生产力了吗?但实际上并非如此,这里所谓的结对编程并非是个人在编程,另一个在看着,另外一个人也同样起着很重要的作用,他的大脑也在不停地运转,他需要帮助编码的人找到低级的失误,防止其编码出现方向性的错误,特别是在出现一个正在编码的人不擅长解决的问题的时候,他会直接拿过键盘,和其交换角色,直接来进行编码。

这样做的好处也许只有在实践了之后才能够体会到,他不但能够避免一些错误的发生,而且能够通过直接的讨论来解决一些容易产生歧义的问题。而且两个人的思路碰撞出来的火花,能够更加快速的解决问题。而且,在交流的过程中,大家的水平也会有很快的提高。结对编程的过程也是一起学习的过程。(只可惜我这里只有一个人,没有办法长期实践,但只要有机会我就会努力的)

3)       研发人员和管理人员的交流

在一个项目组里面,管理人员和研发人员之间的关系是影响项目的一个很重要的因素,假如处理不好的化,可能会直接导致一个项目的失败。而管理人员所具备的素质更是需要很高的。假如是个从技术人员转型的管理者,那么他的管理能力需要很大的提高,否则就会因为管理能力的缺乏而导致项目的混乱。而对于一个单纯的具备管理技能的人来说,怎样能够得到技术人员的佩服是十分重要的,否则根本就无法使研发人员听从管理,那么他的位置也就岌岌可危了。

而且,假如研发人员能够和管理人员进行好的交流,那么他们的工作环境就会得到很大的改善,并不一定要很豪华的房间和高级的家具,只需要一个能够很舒服工作的环境,就能够让一个团队的战斗力得到很大的提升。而且,对于一个项目的计划和预算,假如研发人员能够提出自己的想法,就会避免最后争取到了项目却最终得不到利润的情况的出现。

管理人员也应该主动的听取研发人员的意见,很多的研发人员都是一些比较内向的人,假如不向他们询问,他们只会将自己心中的不满埋在心中,最后的结果是突然的爆发,然后辞职离去,造成重大的损失。

二、简单

1)       设计的简单

在极限编程的过程中,提倡一种简单设计的实践。这样做的原因是由于过多的设计文档会使我们浪费太多的时间在上面,而且设计文档没有不修改的,可能在项目结束的时候,我们会发现当初的设计文档早已使面目全非了。

所以,我们在最初的设计工作中要做的是明确我们要实现的最重要的功能,然后设计出总体的框架和核心的技术,这些文档从头到尾不会超过十页纸,那样即使有了一些改变,我们也无需花费太多的时间来进行修改了。特别是在有了修改之后,我们无需费很大的力气去让代码和文档完全一致了。

但是,简单的设计并不意味着这些设计是可有可无的,相反,那简单的几页纸更加重要,因为一个项目的核心内容都在上面,所以在编写的过程中一定要慎重。

2)       编码的简单

编码的简单表现在迭代的过程中,在极限编程的过程,并非要一下子实现任何需要的功能,也无需一下子就完成以后不再改变,相反,变化在极限编程中是被提倡的。我们能够先简单的实现一点功能,然后添加周详的内容,再后对程式进行重构,最终的代码将是很简单的,因为依照重构的原则进行修改了之后,任何的类和函数、过程都是很简短而非冗长的,每一个模块完成的功能是很明确的。

但是,不要把简单和随意等同起来。尽管我们要实现简单的编码,依然要有编码的标准,使得任何的人都能够很容易的看懂我们编写的程式。其他象属性要使用名词来定义,过程要使用动词来开头的标准也是很有用的,我们应该遵循。

3)       注释的简单

在某些项目中,注释需要是很严格的,甚至于规定在一个程式中注释量必须要达到一个百分比。这个初一看起来很有道理,因为注释能够让我们更好的理解程式的功能,但是细想一下,却完全不是那么一回事。

曾有人说过“一般的程式员能够编写出电脑能看懂的程式,而一个真正的高手能够编写出普通人也能够看懂的程式”。的确是那样,和其让注释来解释程式,不如在给变量和过程、函数起名的时候用大家都能够理解的,那样即使没有太多的注释,另外的一个程式员想要读懂您写的程式也不是一件很困难的事情了。

所以,在编写代码的过程中应该尽可能的使用代码本身来说明问题,而非借助注释的帮助,我们要编写的是代码,假如里面带有太多的无关轻重的代码,一方面会浪费我们的时间,还可能引起歧义;另一方面向微软的Windows源代码里面充满的发牢骚的注释就更不应该了。那些注释只是会给阅读代码的人带来分散注意力的效果了。

4)       测试的简单

通常我们的项目假如是按照瀑布式研发的化,测试会全部放在编码完成之后,其中包括单体测试,集成测试,功能测试连同验收测试等等,而且大多数的测试是通过手工来完成的。所以依据经验来说,假如编码使用了20%的时间,测试至少要用掉40%以上的时间。而且在测试的过程中,更有好多问题需要修改,这也是导致测试耗费了大量时间的原因。

而在极限编程中,测试是通过编写测试代码来自动化完成的。特别是在一些面向对象的编程环境中,我们能够使用xUnit工具来快速、有效的进行单体测试。而且编写这些单体测试代码甚至能够是在正式编码之前。每一次修改了程式之后,都要运行测试代码来看程式是否有问题。而且对于程式的集成,极限编程提倡的是持续集成,也就是不断的将编写好的通过了单体测试的代码模块集成到编写完毕的系统中,在那里能够直接进行Test Suit的集成测试,从而确保代码不会影响到整个系统。

我们能够看到,极限编程中的编码和测试都是一小步一小步的进行的,这样就方便我们及时的发现并修改出现的错误。而自动化测试工具确保了我们的工作的效率,使我们避免了过多重复的工作。

 

  十二条原则:

  1、角色定位:极限编程把客户非常明确地加入到开发的团队中,并参与日常开发与沟通会议。客户是软件的最终使用者,使用是否合意一定以客户的意见为准。不仅让客户参与设计讨论,而且让客户负责编写拥护故事(User Story),也就是功能需求,包括软件要实现的功能以及完成功能的业务操作过程。用户在软件开发过程中的责任被提到与开发者同样的重要程度。

  2、敏捷开发:敏捷开发追求合作与响应变化。迭代就是缩短版本的发布周期,缩短到周、日,完成一个小的功能模块,可以快速测试、并及时展现给客户,以便及时反馈。小版本加快了客户沟通反馈的频率,功能简单,在设计、文挡环节大大简化。极限编程中文挡不再重要的原因就是因为每个版本功能简单,不需要复杂的设计过程。极限编程追求设计简单,实现客户要求即可,无需为扩展考虑太多,因为客户的新需求随时可以添加。

  3、追求价值:极限编程把软件开发变成自我与管理的挑战,追求沟通、简单、反馈、勇气,体现开发团队的人员价值,激发参与者的情绪,最大限度地调动开发者的积极性,情绪高涨,认真投入,开发的软件质量就大大提高。结对编程就是激发队员才智的一种方式。

  极限编程把软件开发过程重新定义为聆听、测试、编码、设计的迭代循环过程,确立了测试->编码->重构(设计)的软件开发管理思路。

  极限编程的12个实践是极限编程者总结的实践经典,是体现极限编程管理的原则,对极限编程具有指导性的意义,但并非一定要完全遵守12个实践,主要看它给软件过程管理带来的价值。 转自项目管理者联盟项目管理者联盟  1、小版本。为了高度迭代,与客户展现开发的进展,小版本发布是一个可交流的好办法,客户可以针对性提出反馈。但小版本把模块缩得很小,会影响软件的整体思路连贯,所以小版本也需要总体合理的规划。

  2、规划游戏。就是客户需求,以客户故事的形式,由客户负责编写。极限编程不讲求统一的客户需求收集,也不是由开发人员整理,而是采取让客户编写,开发人员进行分析,设定优先级别,并进行技术实现。当然游戏规则可进行多次,每次迭代完毕后再行修改。客户故事是开发人员与客户沟通的焦点,也是版本设计的依据,所以其管理一定是有效的、沟通顺畅的。 项目管理培训

  3、现场客户。极限编程要求客户参与开发工作,客户需求就是客户负责编写的,所以要求客户在开发现场一起工作,并为每次迭代提供反馈。 bbs.mypm.net

  4、隐喻。隐喻是让项目参与人员都必须对一些抽象的概念理解一致,也就是我们常说的行业术语,因为业务本身的术语开发人员不熟悉,软件开发的术语客户不理解,因此开始要先明确双方使用的隐喻,避免歧异。 项目经理圈子转自项目管理者联盟  5、简单设计。极限编程体现跟踪客户的需求变化,既然需求是变化的,所以对于目前的需求就不必过多地考虑扩展性的开发,讲求简单设计,实现目前需求即可。简单设计的本身也为短期迭代提供了方便,若开发者考虑“通用”因素较多,增加了软件的复杂度,开发的迭代周期就会加长。简单设计包括四方面含义:1、通过测试。2、避免重复代码。3、明确表达每步编码的目的,代码可读性强。4、尽可能少的对象类和方法。由于采用简单设计,所以极限编程没有复杂的设计文档要求。

  6、重构。重构是极限编程先测试后编码的必然需求,为了整体软件可以先进行测试,对于一些软件要开发的模块先简单模拟,让编译通过,到达测试的目的。然后再对模块具体“优化”,所以重构包括模块代码的优化与具体代码的开发。重构是使用了“物理学”的一个概念,是在不影响物体外部特性的前提下,重新优化其内部的机构。这里的外部特性就是保证测试的通过。

  7、测试驱动开发。极限编程是以测试开始的,为了可以展示客户需求的实现,测试程序优先设计,测试是从客户实用的角度出发,客户实际使用的软件界面着想,测试是客户需求的直接表现,是客户对软件过程的理解。测试驱动开发,也就是客户的需求驱动软件的开发。

  8、持续集成。集成的理解就是提交软件的展现,由于采用测试驱动开发、小版本的方式,所以不断集成(整体测试)是与客户沟通的依据,也是让客户提出反馈意见的参照。持续集成也是完成阶段开发任务的标志。 blog.mypm.net

  9、结对编程。这是极限编程最有争议的实践。就是两个程序员合用一台计算机编程,一个编码,一个检查,增加专人审计是为了提供软件编码的质量。两个人的角色经常变换,保持开发者的工作热情。这种编程方式对培养新人或开发难度较大的软件都有非常好的效果。 10、代码共有。在极限编程里没有严格文档管理,代码为开发团队共有,这样有利于开发人员的流动管理,因为所有的人都熟悉所有的编码。

  11、编码标准。编码是开发团队里每个人的工作,又没有详细的文档,代码的可读性是很重要的,所以规定统一的标准和习惯是必要的,有些象编码人员的隐喻。

  12、每周40小时工作。极限编程认为编程是愉快的工作,不轻易加班,今天的工作今天做,小版本的设计也为了单位时间可以完成的工作安排。

 

测试驱动开发

 

测试驱动开发(TDD)改变了编码的过程,并且这种改变不仅是可能的,同时也是值得去做的。开发包括三方面的活动:编写测试用例,编码并进行测试,重构代码以消除重复代码使其更简单、更灵活、更容易理解。

这个过程会频繁地重复,每次进行测试均是为了保证产品的正确性。设计、编码和测试三者之间的鸿沟将不再存在,这样可以促进你对整个环境更好地理解。因此,你的设计(和编码)将随着项目的成熟逐步得到改善而非降低。

使TDD更加有效的原因是单元测试自动化,而且这些自动化单元测试的工具可以从Internet上免费获得。虽然没有简化功能版的商业产品,但开发人员可以合理地使用这些高质量的软件。本文将指导你怎样获得和使用Nunit并通过C#(或者任意一种基于Microsoft .Net Framework的语言)开发实践TDD。注意到类似的工具对于C++和Java开发人员也是可用的,因为他们支持大多数语言和操作系统。这些工具与极限编程紧密地结合起来,扩大了TDD应用的范围。

为什么设计会降低性能

大多数传统软件开发过程是基于你在设计之初正确的设计与估计,并通过开发以形成完美的产品。这种开发方式保证了产品的统一性和之间的最小差异。然而这一过程却忽略了交流与反馈,同时也不利于生成错误信息(测试失败),并因此制定策略采取相应的措施(修复设计),而这些正是TDD所重视的。

为什么我们不能在开始就获得正确的设计?因为在开发时,处于项目之初的我们无法获得有关这个软件完整的知识。迭代式开发虽然也认同这一事实,并帮助你在项目开发初期识别一些重要的问题,而不是把这些问题留在后期解决。然而,迭代法无法终止开发过程,使你回到设计阶段来解决问题,哪怕这只是因为一个命名糟糕的公共类名。没有人愿意关注这些细小的问题,且不幸地,这种设计过程也禁止这种更新,因为在各个阶段不停重复的代价太高。

传统开发过程中的这些小问题一旦积累起来,会导致大问题的发生。你或许认为与其将时间耗费在这些对功能影响不大的细枝末节上,还不如将精力放在更重要的环节。然而,这个命名糟糕的公共类在代码中保留的时间越久,则相关的使用也会越来越多,改变起来就越来越困难。之后,团队会在编码的时候非正式地修改这些问题,很快,在计划发布整个产品时,你会做大量的工作尽量使代码和设计文档保持一致。对于这种情况来说,你之前的设计是没有价值的,因为代码本身就说明了设计。

测试驱动开发允许你推迟决定,直到你更好地理解了问题之所在。当你只是了解到产品开发的一些基本信息时,你不必设计出完美的体系架构。这对于传统软件开发中已经确定的理念来说,是一种挑战,从某种角度来说,甚至是违反常规的。因此我们建议你以一种开放的思想来尝试TDD,你会发现TDD的强大。

TDD的另一个好处是更容易掌握。你开发的一系列测试说明了代码的运行机制,这种程序自我编档的方式促进了交流。它要求你通过概览测试用例并阅读自己的代码来获得反馈,从而有利于你创建的对象和组件更趋于松散耦合。同时,TDD可以推迟你的设计决策,从而简化设计,使你能集中精力修正设计的问题。最后,它通过给出的一系列测试减轻你编码的压力,因为当测试发生中断时,它可以立即告诉你究竟发生了什么。

简介

要真正理解TDD,唯一的办法就是实践。让我们从一个简单的例子开始,这个例子并不要求任何特殊的工具。我们要写一个小程序,来帮助我们规划住宅前的一块长方形的草坪,但在设计之初,我们需要作两个测试:当长度为3,宽度为2时,计算得到的面积应该为6;同时计算其周长应该为10。

写下这些测试有助于我们将焦点集中到程序的一些要点。在这个例子中,似乎创建一个对象来模拟问题域是合理的,因此我们创建了一个类,取名为Quad,来代表我们要建造的长方形草坪,并在一个简单的控制台应用程序中实例化它。下面就是我们实现的步骤:
1、启动桌面的Visual Studio,选择文件 | 新建 | 项目,选择C#项目类型中的“控制台应用程序”。为项目取名为“QuadApp”,然后点击OK。
2、在主函数中输入下列几行代码,创建Quad实例,并通过Assert断言,当长宽各为3和2时,返回的结果应该是6。
 static void Main(string[] args)
 {
     Quad q = new Quad();
     System.Diagnostics.Debug.Assert(q.Area(3,2) == 6);
 }
3、选择项目 | 添加类,在对话框中输入“Quad.cs”,点击“打开”以创建类。
4、输入下列代码,创建在Main函数中用到的Quad类的成员方法Area:
 public class Quad
 {
     public int Area(int length, int width)
     {
         return 0;
     }
 }
5、选择生成 | 生成QuadApp 菜单项,此时编译应该是成功的,然后当你运行该程序时,会报告错误,因为此时返回的面积值不等于6。
6、指定Area方法的返回值为6,再重新编译程序,并运行,此时没有错误发生。
7、改变Area方法的实现,返回值用输入参数的乘积来代替起初指定的返回值常量。再次重新编译程序,并运行测试。

第二个测试(计算周长为10),与前面的方法相似。首先在主函数中通过assert写测试代码,接着执行第4到第7步,只是方法名用Perimeter而不是Area。当你在第七步中试图改变设计时,你可能会得出一个结果,就是可以通过构造函数来传递长和高的值,并将其存储为对象的属性,而不是通过成员方法的参数来传递。设计改变后的代码如下:
public class Quad
{
    private int m_length;
    private int m_width;

    public Quad( int length, int width)
    {
        m_length = length;
        m_width = width;
    }
    public int Area()
    {
        return m_length * m_width;
    }
}

重新编译程序,并运行之,以保证你在写代码是否因粗心而漏掉了什么。

我们通过一种最简单的形式完成了演示TDD工作原理的练习。下面是每一步的总结:
1、写一个失败的测试:我们选择了一个最容易实现的测试(虽然在这个例子中所有的都很简单)来实现。接下来,我们写了个Quad类的最简单实现,以保证程序能够通过编译。然而,当我们执行程序时,调试窗口会出现assert的错误信息,因为返回的面积值不是6。
2、修改代码以通过测试:要修改代码,首先我们做一个最简单的方法,就是直接返回常量值6。该值刚好使程序能够运行且通过测试。接着下一个测试必须使用不同的参数值,迫使我们实现正确的算法,以通过所有的测试,即使我们还没有开始下一个环节的重构。
3、重构代码:既然在我们的程序中达到了正确的预期目的,我们应该试图去掉一些重复代码使代码更易维护,更简单,更灵活,也更易于理解。我们可以发现这种改变在运行测试时,并没有影响到程序的行为。

修改代码使其易于维护,要求不能修改程序可观察的行为,这个思想并不是全新的理念。然而这里重要的是通过重构,而不是一遍一遍地整理代码。重构必须是软件开发活动中一个关键的环节,通过合适的工具,并采取系统的方法,可以逐步地改善代码的质量。通过执行贯穿于产品整个生命周期的许多次小的重构,最后达致最完美的目标,从而使得软件易于维护。

倘若必要的反馈要求你的设计(和代码)在产品开发中得到改进,则重构是测试驱动开发中不可缺少的步骤。当然你也可以不执行TDD方式,而进行重构以去除重复代码,使其更简单、灵活、更易于理解。无论你采取什么开发方式,你都需要将重构作为一种常规的活动,以保证你的更改是有效的。

重构的力量在于其能够减少因为改变正在工作的代码所带来的危险。好的工具能够帮助你减少潜在的危险,而同等重要的是要采取一系列严谨的步骤和正确的结构与规则。Martin Fowler的大作《重构:改善既有代码的设计》对于重构有非常精彩的描述。他定义了一系列非常有用的重构模式,并提供许多相关的样例指导你怎样进行重构。

目前,Visual Studio.Net除了提供查找和替换功能外,对重构没有太多的支持。然而,相信在不远的将来MS会提供更多有力的工具,使你能完成更多复杂的工作,例如符号的重命名,在编译-生成级上更改类名,而不是在你的源文件中作替换操作。你甚至可以期待这样一天的到来——当你选中一段代码后,通过Visual Studio提供的菜单,直接应用重构模式,然后继续下一步工作。你有充足的信心保证你的程序是更加易于维护的,而不会引入bugs或者会改变其可观察的行为。

经验总结

迄今为止,你应该明白一条最重要的道理就是TDD很简单。事实上,TDD能帮助你更好地理解任意一门新的语言、新的技术和组件。通过TDD,你可以决定将要采取的步伐的跨度。有经验的专家可以采取大的步伐,以避免中间环节。一旦发现开发有误,可以令过程回滚,采取更小的步伐重复前面的过程。关于TDD我们需注意下列事项:
1、测试使代码文档化(The tests document the code)。从测试的类Quad中可以清晰地看到这点;
2、随着测试的进行,我们可以把握进度。通过测试,可以验证每个功能是否正确,如此每个人都可以运行测试来了解过程是否如我们说期望的那样被良好的建立。
3、测试让人对代码的修改充满信心。即使你是一个C#的新手,也可以象专家那样老练的编程,运行所有的测试确保你所做的没有违反既定的规则。
4、类似于“将长度和宽度以参数的形式传递给面积和周长,而非在构造函数里指定”这样的错误,可以通过重构来修正。
5、当你将TDD应用到现实世界时,你可以生成许多测试,并通过如Nunit的工具来组织它们。

使用Nunit使测试自动化

TDD可能会生成成千上万个测试。假使项目中的每一个人都在周而复始地重复“写测试,修改代码,重构”,当然有必要使你的测试工作更加有效。如Nunit这样的测试框架就是设计用来帮助你提高效率的利器。它允许你象在Visual Studio中建立项目的方式,将你的测试用例装载到NUnit中,以合理地安排各自的项目。你可以将所有测试用例以层次结构显示出来,以单用例或测试包(一组测试用例)的方式运行测试,观察执行后的结果是成功(显示绿色进度条)还是失败(显示红色进度条),同时还将显示每次失败的详细信息。



图一:NUnit中的测试状态

另外,NUnit还提供了一些特殊的方法,使你能够在测试包开始和完成时执行初始化(initialize)和清除(clean up)被不同的测试用例所共享的“测试套件(test fixtures)(例如:长文件,数据库连接,公共链接)”。你也可以定义Setup和TearDown方法使其分别在执行测试前和完成后运行。它可以帮助你通过重新更改每个用例间的系统设置,使特定的用例从其它用例中独立出来。

Nunit允许你用与应用程序相同的语言和环境来写测试用例。不需要学习特定的测试语言,使开发测试迅速而直接。同时还有一个完整的Assert类提供特定用例失败后的详细信息。

最后,NUnit还可以作为控制台应用程序运行,只需要通过命令方式就可以输出结果。支持自动化创建合法的进程,举例来说,这样你就可以重建应用程序,并运行完整的测试包。同时,NUnit控制台应用程序还可以生成Xml格式的测试结果日志。

有着软件测试背景的人们应该能认识到这些特性的价值,但即使你不具备这些经验,也能清楚地明白它能为作为开发人员的你做出多大的贡献。不过,不管你是专家还是新手,要明白NUnit是怎样帮助你组织测试用例的,最好的办法还是实践。你可以到http://www.nunit.org/去下载NUnit。关于使用该软件的详细细节可以从其许可文件中获得。

本文使用的NUnit是2.1 beta版,你也可以去下载最新版本的MSI文件(大约1.5MB)。有了这个文件后,你可以双击它进行Windows下的安装。它可以在.Net Framework1.0和1.1版本下运行,当然你也可以参考NUnit文档获得有关系统需求的更多信息。

安装好后,选择“Test”的子菜单项运行你的测试包,以确保产品是否安装正确。单击了菜单项后,等待你桌面上的NUnit打开后,然后单击“Run”按钮。

不到一分钟的时间,treeview下的所有节点都将显示为绿色,这表明测试正常执行并通过。测试运行的数目会在状态条中显示出来,同时会显示执行测试所花的时间以及失败用例的数目。有时候你可能会得到一些显示为红色的节点,这表明测试失败,你可能需要重新安装NUnit,或者从NUnit.org站点获得建议。

在NUnit中使用测试用例

NUnit有一些附带样例,你可以使用他们其中的样例,如Money项目,来学习使用NUnit的开发流程。

在你的桌面上启动Visual Studio,选择文件 | 打开 | 项目 菜单,在NUnit的安装目录(Program Files\NUnit)的子目录src\samples\money下找到Money.csproj文件,单击“打开”按钮并等待Visual Studio将其加载。在解决方案资源管理器窗口中,查看引用文件夹中nunit.framework引用是否正确。选择Build | Build Money编译Money项目。编译应该成功通过,没有错误和警告。

在桌面上启动NUnit。Treeview中应该是空的——如果不是,请选择File | Close菜单关闭所有已存在的项目。然后在File菜单中选择Open菜单项打开Money.csproj文件,就像你在Visual Studio中打开该文件一样。现在Treeview中应该包含测试样例的集合。

选择Treeview(文件Money.csproj)中的根节点,单击NUnit右侧的“Run”按钮。测试用例的所有包均被执行,并恰好获得一个失败的测试——MoneyBag Equals。

选择MoneyBag Equals测试用例(红色的叶结点),并定位到该文件的出错行,此时NUnit的右侧下方会显示相关信息,如图二:



图二:NUnit错误跟踪

切换到Visual Studio,打开该文件并定位到出错行。将该行注释掉(在行开始处插入//)并重新编译(生成 | 重新生成Money),此时编译将完全通过,没有错误和警告。

现在再切换到NUnit,注意到Treeview的所有节点均为灰色,这表明你更新后的程序还没有运行。选择Treeview的根节点,单击“Run”按钮。你可以看到所有的测试均通过且没有错误。

以上就是NUnit的全部工作方法。现在可以为你自己的项目创建测试用例了。

测试用例

测试用例定义了一系列输入,同时也可以定义一系列输出,以验证被执行的某些程序特性没有引入任何Bug。测试可能会失败是因为产品里有Bug,但也可能是测试代码自身有Bug——可以称为false negative。相似的,测试能通过也有可能恰好是因为测试中有Bug,而非你的产品准确无误,我们称为false positive。[译注:意思是测试中有Bug,而产品中也有Bug,巧的是这两处的Bug恰好负负得正,抵消了Bug所带来的影响,反而使测试通过。]好的测试应该避免出现false negatives,且绝对不能包含false positives。另外,好的测试包不仅要测试正确的场景,也要测试错误的用例。

测试对象和其环境在测试用例被执行前总是保持相同的状态,因此一旦有必要就需重复执行并给出恒定的结果。当测试失败时,定位到错误的地方就能获得相关的信息。通过这些信息就能推断出问题能否被纠正。这就是为什么要求你对每个测试用例都要验证其合法性的原因所在。

现在我们为Quad例子写一些测试用例来实践上述的原理。在Visual Studio和NUnit中打开Quad项目,和起初打开Money项目的方法一样——选择文件 | 打开菜单,并定位到QuadApp.csproj。选择项目 | 添加引用,找到文件nunit.framework.dll(在NUnit安装目录的bin子目录下),单击确定,添加引用nunit.framework。

在Visual Studio中创建QuadTest类:选择项目 | 添加类,输入名字QuadTest.cs,然后单击“打开”。输入下列代码到QuadTest中:
using System;
using NUnit.Framework;

namespace QuadApp
{
    [TestFixture]
    public class QuadTest
    {
        [TestFixtureSetUp]
        public void DoTestSuiteSetUp()
        {
            Console.WriteLine("TestSuiteSetUp");
        }
        [TestFixtureTearDown]
        public void DoTestSuiteTearDown()
        {
            Console.WriteLine("TestSuiteTearDown");
        }
        [TearDown]
        public void DoTestCaseTearDown()
        {
            Console.WriteLine("TestCaseTearDown");
        }
        [SetUp]
        public void DoTestCaseSetup()
        {
            Console.WriteLine("TestCaseSetup");
        }
        [Test]
        public void Area()
        {
            Quad q = new Quad();
            Assert.IsTrue( q.Area(2, 3) == 6);
            Console.WriteLine("Area");
        }
        [Test]
        public void Perimeter()
        {
            Quad q = new Quad();
            Assert.IsTrue( q.Perimeter(2, 3) == 10);
            Console.WriteLine("Perimeter");
        }
    }
}

编译后,在NUnit中运行测试。单击NUnit右侧的Console.Out标签,可以查看执行的顺序。

现在你可以看到,不同的Setup和TearDown方法是怎样允许你控制测试环境,从而保证你的测试用例能根据需要而重复执行并给出恒定的结果。这些方法将帮助你确保运行一个测试用例而不会影响到另外的用例(测试用例独立性test case isolation)。测试独立性的最佳证明就是具备以任意顺序运行测试用例的能力。试着改变一下测试用例的顺序,以确认他们是否具有独立性。

测试套件和测试包

一个测试套件是一个或多个测试用例共享的一个对象,这些测试用例都与测试对象的初始化或提供相应资源有关。在NUnit的术语中,测试套件是一个具备特性(attribute)[TestFixture]的类,并提供下列方法:
1、[Test]方法形成不同的测试用例。这些测试样例应该是测试的核心操作,并且与其它测试无关。
2、[SetUp]和[TearDown]方法提供测试用例间的环境设置。它们分别在测试开始前和结束后执行。
3、[TestFixtureSetUp]和[TestFixtureTearDown]方法要求对象被测试用例共享。它们分别在测试套件开始和结束时执行。

你希望运行测试的任意对象都可以在[TestFixture]类(被所有测试用例共享)里创建实例变量,也可以在方法(对于单个测试用例中是私有的)中创建局部变量,这就是为什么要描述为测试套件(test fixture)的原因。然而,你也可以将NUnit的[TestFixture]看作是组织测试包的一种方法,它将测试包组织为每个类,形成treeview的每一个分支。

既然你已经能够为自己的项目创建测试包,并且通过NUnit来运行它们,那么就让我们开始TDD之旅吧。当然,首先还要看看我们在开发工作中可能会遇到的问题。

在现实世界中使用TDD

在使用TDD时,首先要考虑的是:既然商业产品是设计用来组织你的程序的,那么产品代码就应该很容易从代码中分离出来,以用来测试。在产品开发期间,你可以运行你的测试程序,而为了发布产品,你仍然能够轻易地将其从代码中移除。

你可能会遇到的另一个难题就是测试GUI应用程序的困难,因为GUI应用程序是被鼠标和键盘输入而驱动的。举例来说,你应该怎样写测试程序来激活用户点击dropdown list控件的事件呢?如果用户选择的是国名列表,那么又该怎样验证选中的内容呢?

解决这些问题的方案就是:将代码根据编译、测试和部署分类为不同的组件。举例来说,我们不应创建一个作为主应用程序的Quad类,生成相同的可执行文件;而应该允许这个类能够被包含在一个单独的库文件(.dll)中。[译注:实质就是将Quad以类库的形式创建,而非应用程序。] 这样我们就可以开发单独的测试程序和域程序作为各自的可执行文件(.exe),并共享同一个包含Quad类的公共库文件(.dll)。注意,如果你仅仅创建了库,NUnit和你的测试包会被当作接口使用,而不是所需的单独的业务外观(separate harness)。

记住这个原则:“使主程序尽量简单,而将复杂的业务作为类放到库中”。它将帮助你解决测试GUI应用程序的问题。TDD的其中一个原则就是你不能测试第三方代码,因此你没有必要测试GUI框架类,虽然当框架相对简单的时候,有时会用来测试接口。这意味着你可以捕捉类里的用户事件,因为你知道这个类是怎么工作的;然后再将其传递到类里进行处理。此外,你也可以将测试程序和域程序从单独的可执行文件中分离出来,而该执行文件则共享了你竭尽心力开发的公共库。

让我们看看怎样实际运用TDD来开发包含有Combobox控件的简单窗体应用程序。

开发一个测试和创建库

在Visual Studio里选择文件|新建|项目菜单新建一个控制台应用程序,取名为CountryTest。然后如第一个例子那样,为项目添加nunit.framework引用。选择项目|添加类菜单,输入名字CountryBoxTest.cs,点击“打开”按钮,创建类CountryBoxTest。然后在CountryBoxTest类中输入下列代码:
[TestFixture]
public class CountryBoxTest
{
    [Test]
    public void CheckContent()
    {
        CountryLib.CountryList list = new CountryLib.CountryList();       

        Assert.AreEqual("UK", list.GetCountry(1));
        Assert.AreEqual("US", list.GetCountry(2));
        Assert.AreEqual("CH", list.GetCountry(3));
        Assert.AreEqual(null, list.GetCountry(4));
    }
}

生成项目。编译失败,因为CountryList类不存在。

使用Visual Strudio,选择文件|新建|项目(添加到业已存在的解决方案中),创建一个新的类库,命名为CountryLib。选择项目|添加类输入名字CountryList.cs,单击“打开”,创建新类CountryList。然后将下面的代码输入到CountryList中,并编译。
public class CountryList
{
    public String GetCountry(int No)
    {
        return null;
    }
}

首先我们为项目添加引用CountryLib.dll,然后重新编译控制台应用程序CountryTest。此时项目运行正常,因为我们已经创建了CountryList类。

在NUnit中打开CountryTest.csproj,并运行测试。显示失败,因为CountryList.GetCountry方法返回null。切换到Visual Studio,在类CountryList中修改GetCountry方法,使其返回测试所需要的值——仅仅添加一些字符串常量,如下所示:
public class CountryList
{
    string ne = "UK";
    string two = "US";
    string three = "CH";

    public string GetCountry(int No)
    {
        if ( No == 1 )
            return one;
        if ( No == 2 )
            return two;
        if ( No == 3 )
            return three;
        else
            return null;
    }
}

这段代码主要是为了程序正常进行,而写的最简单的例子。重新编译CountryList类库,并切换到NUnit,然后运行测试——运行成功。

对CountryList进行你认为必要的重构,每次重构后都进行测试,以检查是否出现错误。你可能认为在ArrayList中存储字符串可以使代码更简单,容易,更利于理解,同时避免重复,但是你也可以选择其它解决方案。

创建GUI

使用Visual Studio选择文件|新建|项目(添加到现有的解决方案中)创建新的Windows应用程序,并为项目添加引用CountryList.dll。接下来,在工具栏中拖动combobox控件到主窗体上。然后将下面的代码输入到CountryApp中,并编译应用程序。
private void InitializeComponent()
{
    ...
    CountryLib.CountryList list = new CountryLib.CountryList();

    String country;
    int index = 1;

    while ((country = list.GetCountry(index++)) != null)
       this.comboBox1.Items.Add(country);

    ...
}

现在你的项目已经有了基本的结构,它允许你采用TDD技术通过进一步开发CountryList类库来创建GUI应用程序。考虑一下CountryList怎样才能处理用户接口事件(鼠标单击,键盘),并通过这个方法使其能够被CountryTest测试,并被CountryApp使用。对于CountryList,你可能会有比本文的示例更好的解决方案,不过本示例试图说明:测试驱动开发能够改善设计,即使设计已经成熟。之前你不必一定要拿出完美的设计方案,这对于开发人员来说,无疑是一个福音。

结论

大多数设计都是从上而下创建,并基于可观察的特性进行分类,促进对问题的理解和解决。换句话说,我们试图创建层次结构的对象,它模型化了问题域。相反,TDD则是从下而上,通过对一些小问题,依次实施一系列简单的解决方案,最后演进为设计。

重构确保了设计集中于一个好的解决方案而不是逐渐脱离设计。如果你认为这是设计之根本,你就对了。TDD将在这十年潜在地改变软件开发的方法,就像过去十年面向对象技术所做到的那样。

NUnit提示:当我们使用Visual Studio打开NUnit附带的预先建好的测试样例时,你可能会发现引用nunit.framework.dll显示未找到,那么请打开解决方案资源管理器窗口的引用文件夹。此时你需要删除已经存在的引用(选择文件,右击“移除”),然后再次添加(选择引用,右击“添加引用”,浏览并定位到该文件,单击“确定”)。nunit.framework.dll文件可以在NUnit安装文件夹的bin子目录下找到。

 

posted @ 2012-04-20 12:29  书奎  阅读(477)  评论(0编辑  收藏  举报