在过去的几年中,单元测试逐渐成为我编写软件的核心内容,在这里要感谢一种叫做极端编程-XP(注1)(见“资源”一节)的简便程序设计方法。这种方法要求我为新加入的每个函数都编写单元测试,并且维护这些测试。没有通过单元测试,我就不能将任何一个的代码加到模块中。在代码基数增长的同时,这些测试允许开发者有依据地将改变集成起来。起初,我认为这些单元测试就足以应付全局,没有必要涉及到功能测试。噢,又错了。功能测试和单元测试完全不同的两者。我花费了很长的时间才理解到两者的区别,以及如何将它们结合起来,用以改进开发进程。
本文探讨了单元测试和功能测试之间的差别,同时介绍在你的日常开发的过程中如何来利用它测试和开发过程作为一个开发人员,测试如此之重要,以至于你甚至应该花费几乎所有的时间来完成它。它不仅需要只被划分为开发过程中的某个特定阶段。显然,它不该是在你把系统交付给客户之前完成的最后一项任务。然而,你又如何得知它在何时结束呢?或是你如何得知是否因为修改一个微小的bug而破坏了系统的主要功能呢?或是系统可能会演化成超乎现在想象的模样?测试,单元的和功能的都应该是开发的过程中的一部分。
单元测试应成为你编写代码的核心环节,尤其当你在从事一个项目时,紧张的时间约束你的开发进度,你也很想让它是在可控的有序下进行。我希望测试也是在你编写代码之前编写测试时的重要内容。
一套适用的单元测试应具备以下功能:
说明可能的最佳适用设计
提供类文档的最佳格式
判断一个类何时完成增强开发人员对代码的信心
是快速重构的基础
在系统中自然要包含单元测试所需的设计文档。重新阅读它,你会发现这是软件开发进程中的圣杯,文档跟随系统的变化而逐步演化。为每一个类提供完备的文档比起为它提供一系列的使用框架,或是一系列可控的输入要好得多。这样,设计文档就会因为单元测试的逐步通过而随时更新。
你应该在你编写代码之前完成编写测试的工序。这样做会为测试所涉及的类提供设计方案,并促使你关注代码中更小的程序模块。这种练习也会使设计方案变得更加简单。你不能试图去了解将来的情形,去实现不必要的功能。编写测试工作也会让你清楚类会在什么时间结束。可以说,当所有的测试通过时,任务也就完成了。
最后,单元测试会提供给你更高级别的依据,这绝对会满足开发者的。如果你在改动代码的同时,进行单元测试,你就会在你破坏的同时立即察觉到事态的发生。
功能测试甚至比单元测试更加重要,因为它们说明了你的系统就要预备发布了。功能测试将把你的工作系统放置于一个可用的状态中。
一套适用的功能测试应具备以下功能:
有效地掌握用户的需求
向项目组成员(包括用户和开发者)给出系统面临这些需求的依据
功能测试要在有效地情况下掌握用户的需求。而传统的开发者是在使用的过程中发现需求的。通常,人们赞同使用项目工程并且花费相当的时间去重新定制它们。当它们被完成时,它们所得到的仅仅是一堆废纸。功能测试雷同于自行生效的使用项目的情况。极端程序设计方法(ExtremeProgramming)能够说明这种概念。XP
的说法就是对未来发生在用户和开发者之间的交流技巧的描述。功能测试也是这种交流的结果。而没有功能测试,这种说法也不会建立起来的。
功能测试恰好填充了在单元测试和向项目小组提交的代码依据之间的空隙。单元测试会漏过许多的bug.它可以给出代码中你所需的所有有效部分,它也会给你所需的整个系统。功能测试可以使单元测试里漏掉的问题曝光。一系列可维护的,自动化的功能测试也会有漏网的情况,但是它至少比独立地进行最全面的单元测试要有用得多。
单元测试VS 功能测试
单元测试告诉开发者代码使事情正确地被执行,而功能测试所说的则是代码在正确地发挥功效。
单元测试
单元测试是从开发者的角度来编写的。它们确保类的每个特定方法成功执行一系列特定的任务。每一个测试都要保证对于给定的一个已知的输入应该得到所期望的输出。
编写一系列可维护、自动化、没有测试框架的单元测试几乎是不可能的。在你开始之前,选择一个项目小组都认可的框架。不断地应用它,逐渐地喜欢它。在极端编程的介绍网页上(见资源一节),有很多适用的单元测试框架。我喜欢用的是Juint
来进行Java 代码的测试。</P>
功能测试
功能测试则是从用户的角度来编写的。这些测试保证系统能够按照用户所期望的那样去运行。很多时候,开发一个完整的系统更像是建造一座大楼。当然,这种比喻并不是完全地恰当,但我们可以扩展它,来理解单元测试和功能测试之间的区别。
单元测试类似于一个建筑检查员对房屋的建设现场进行检查。他注重的是房屋内部不同的系统,地基,架构设计,电气化,垂直的线条等等。他检查房屋的某个部分,以确保它在安全状态下,正确无误地工作,即是说,直接针对房屋的代码。功能测试在这个剧本里类似于房屋的主人在检查同样的建设场地。他所期望的是房屋的内部系统正常地运转,并且房屋检查员执行了他的任务。房屋的主人看重的是生活在这样的房屋中会是什么样子。他关注这间房屋的外貌,不同的房间有合适的空间,房屋适用于家庭的需要,窗户恰好位于最佳采光的位置。房屋的主人运行的是对房屋的功能测试,他站在用户的角度上。房屋检查员运行的是单元测试,他是站在建设者的角度上。
象单元测试一样,编写一系列可维护、自动化、没有测试框架的功能测试几乎是不可能的。Junit在单元测试方面做得很好;然而,它在试图编写功能测试时就显得比较松散。Junit
不等同于功能测试。现在已经有满足这个功能的产品问世了,但是我还没有看到它们被应用于开发产品过程里。如果你不能找到一个测试框架的话,就只好自己创建一个了。无论我们在建立一个项目时多么聪明,建立的系统多么灵活,如果我们的产品不能用,我们就是在浪费时间。结论是,功能测试是开发进程中最重要的一部分。
因为两种类型的测试都是必要的,你会需要编写它们的指南。
如何编写单元测试<BR></STRONG>在你开始编写单元测试很容易被激动的情绪感染。最简单的起步方式就是为新的代码创建单元测试。为已经存在的代码创建单元测试是一种比较有难度的开始方式,但是也是可行的。)从新的代码开始,习惯了这样的步骤后,还要坚持重新阅读现有代码,并为它们创建一套测试程序。
就像前面提到过的一样,你应该在你编写要测试的代码之前编写单元测试。如何做到为还不存在的事物编写测试呢?好问题!掌握这个能力需要90%的智力和10%技巧。我的意思是你只需假装是在为已有的类编写测试。接下来,进行编写的工作。最初,你将出现很多语法错误,但是let
it be,不要理会它。紧接着进行单元测试,修改语法错误(即是说,只用你自己定义的测试接口来实现类),再一次进行测试。重复这个过程,每一次都写下充足的代码去修改错误,进行测试直到它们通过为止。当所有的单元测试都通过时,代码才算真正地完成了。
一般地说,你的类应具有开放的单元测试方式。然而,带有直截了当的功能性的方法比如说,Java 语言里的Getting
和Setting 读写方法,就不需要单元测试,除非它们是以“特殊”的方式进行的。接下来的指导就是,当你感到需要对代码中的某些特性添加注释时,同时要编写出单元测试。如果你同很多的程序员一样,厌恶为代码写注释,单元测试就是将你的代码的特性文档化的一种好方法。
将单元测试同被测试的相关的类打包在一起。(这种组织的方式允许每一个单元测试都能够直接访问类中被打包和保护的方法和参数)。要避免在单元测试中用到域对象(domain
object)。域对象就是对于一个应用程序特定的对象。
例如,电子表格应用程序有个工作簿对象,它就是一个域对象。如果你的一个类已经知道了域对象,在你的测试中用到这些对象是很好的。但是如果你的类没有涉及到这些对象,就不要在测试里让它们同类纠缠不清了。不这样做的话,就会产生打包的代码被重用。经常是为一个项目创建的类也可以应用于其他的项目,这样可能会出现直接重用这些类的情况。但是如果针对这些类的测试也用于另外的项目对象,让测试生效会很费时,通常测试不是被抛弃掉就是被重新编写。
以上的一些技巧会让你从中受益,但最重要的是如果你不实际地去做,就永远不会对单元测试有全面、深入的理解。更早地运行测试,并且在整个过程中都在代码中给出全面的依据。当项目进展时,你会随时添加更多的特性。运行测试就会提醒你,实现刚添加的特性会不会破坏已有的东西。
在你已经掌握编写单元测试的技巧之后,你需要重新阅读已存在的代码。的确,为它们编写代码可能会是一场挑战。但是千万不要为了测试的目的而测试。可以说,编写测试是一件紧跟时效的事情,尤其是当你发现要修改一个没有好的测试程序的类时,那就是添加测试的恰当时机。和平常一样,单元测试应该具备类每个方法的特性。实现测试的一个最简单的方法就是,测试的同时一定要注意代码的注释。在单元测试中,不能放过任何一个注释,在描述测试方法的开始就要为单元测试添加大量的注释中。
如何编写功能测试
尽管功能测试是如此重要,它也有个开发过程里丑陋的继生子的坏名声。在大多数的项目里,是由一个独立的工作组来完成功能测试的工作。通常需要一群人在系统中的相互协助才能保证工序的正确运行。这种通常的看法和队伍的组建的做法,都是非常愚蠢的。
功能测试同单元测试相类似。一旦要编写有用户涉入的产品的代码(例如,对话框)时,就要编写测试,但是一定要在实际编写代码之前做。一旦你开始了一项新任务,就要在功能测试的框架里清楚地描述这个任务的内容。你加入的新代码的同时进行单元测试,开发工作就向前持续进行。
当所有的单元测试都进行通过后,再进行最初的功能测试来判断项目是否可以通过,或是需要修改。理想的状况下,功能测试小组的概念应该不存在的。开发者应该同用户一同编写功能测试。系统通过了一系列的单元测试后,负责进行功能测试的小组成员就要改变初试测试的参数,再进行系统的功能测试。
单元测试和功能测试之间的界线<BR></STRONG>一般情况下,很难划清在单元测试和功能测试之间的界限。说实话,一直以来,我就不知道这个界线应该定在哪里。当编写单元测试时,我用以下几个方法来判定单元测试是不是已经变成了功能测试:<BR>如果单元测试超越了类之间的界限,它可能变成了功能测试<BR>如果单元测试变得非常的复杂,它可能变成了功能测试<BR>如果单元测试变得很脆弱(即是说,它已经成为一个测试,但是却因为要迎合不同用户需求的改变而被动地变化),它可能变成了功能测试<BR>如果单元测试比需要测试的代码还要难于编写,它可能变成了功能测试
注意“它可能变成了功能测试”的说法,在这里没有严格的标准。在单元测试和功能测试之间是有界线的,但是你必须自己判定它在哪里。单元测试进行地顺利,特定的测试逾越两者界线的过渡就越明显。
结论
单元测试以开发者的角度来编写,并注重被测试类的特性。当编写单元测试时,利用以下几条指导:
在类代码进行测试之前编写单元测试
在单元测试里掌握代码的注释
测试所有执行特定功能的公用程序(即是说,和Java 语言中的Getting 和Setting
读写方法不同的方法。除非它们是通过一种特殊的方式来完成Getting 和Setting 功能的。)
将所有的测试项目同被测试的类打包在一起,并且分配它们对在模块包内的和被保护成员的访问权限
在单元测试中避免使用某些特定的对象
功能测试也需要从用户的角度出发来编写,并且注重用户所感兴趣的系统功能。选择一个适当的功能测试框架,或是开发出一种,并利用这些功能测试来制定用户们想要的东西。通过这种方式,功能测试的人员可以获得一个自动的工具,并且对使用工具的习惯有了一个好的起点。
将单元测试和功能测试作为开发进程的核心内容。这样做,你就会确定系统在正常运转。如果没有,你恐怕不能保证系统是正常工作的。测试可能不是一件好玩的事情,但是从事单元测试和功能测试会使开发过程里含有更多的乐趣。
资源“利用Ant 和JUnit 改进开发过程”(开发工作,2000 年12 月)揭示了单元测试的益处,尤其是应用了Ant
和Junit 之后。
开始了解极端编程的方法从极端编程的网页上下载各种单元测试的框架。
|