测试驱动的开发——概述
 

2009-11-10 作者:Wellie Chao 来源:theserverside.com

 

一、简介(Introduction)

你可能已经听说了这个新名词:测试驱动的开发(test-driven development),它在广大、各种杂志和网络中程序员常去的地方都非常流行。它究竟是什么呢?测试驱动的开发是一种方法理论,它强调把测试作为开发过程的一个主要部分。要是关心程序质量的话,在部署之前前就应该测试。几乎所有人都使用某种方法进行测试,所以你可能会问:测试不已经是开发过程的一个部分了吗?测试驱动的开发要让测试成为开发过程的主要部分。

二、手工测试(Manual Testing)

早期的软件开发时,许多人的测试方法就是运行程序,通过手工操作来指定输入然后观察输出。现在还有很多软件的开发继续着这种方式。它有一个关键的优点:非常容易学习和理解。如果你有键盘和鼠标,并且会操作图形界面,你就可以测试任何桌面应用。如果你有浏览器,你还可以测试应用。这根本谈不上什么技巧,你可以认为所有开发人员都能做到。手工操作时你看到的应用和最终用户看到的完全相同,这也是一个不错的主意,但并不是真正的优点,因为其它的测试方法也能做到。

手工测试的缺点就实在太多了:

手工测试需要反复进行。每一次修改程序,不管是增加新特征,改变已有行为,还是修改bug,你都必须重新测试被影响的部分,才能保证你增删改的代码不会造成破坏。

手工测试可能带来错误。人类不适合作重复性的工作,特别是这个工作还很烦人的话。人类经常忽略细节,这会导致代码被破坏。更可能造成的问题是:修改这个地方,可能会影响到另一些代码的行为,尽管它们的关系不是很明显。因此,即使你作了非常认真负责的测试,那也只限于你认为修改可能影响到的部分,你还是有可能会在其它地方漏掉一些bug或被破坏的功能,不是因为你不够仔细,而是因为软件开发是综合性的工作,软件的各个部分常常有关联,在现在的软件项目中,任何人都不可能详细了解某段代码所有的依赖和被依赖关系。

手工测试无法对不可视组件进行单独测试。如果你只测你能看到的,那你就没测你看不到的。这看上去是非常简单的一个命题,但它的意义重大。软件是复杂的,为了定位和解决错误或被破坏的代码,大量形态各异的bug需要通过“剥茧抽丝”式的测试去分析每一个组件的行为。对于端软件尤为如此。服务器端程序中,几乎所有的重要代码都在逻辑层,如基于web的程序。表示层测试只能间接的测试业务逻辑,可能忽略某些细节。你当然希望逻辑层和表示层都能很好的工作,但逻辑层正常工作是整个应用能正常运行的基础。

如果你的测试是手工进行的,别人就没法判断你的程序功能是否正确。其他人(例如其他开发人员或者甚至是最终用户)只能接受你的承诺:你作了测试,各方面都满足需求。除了你的书面或口头声明保证你对程序作过测试它满足所有可视的检查外,其他人没办法验证功能正确,除非他们自己辛苦的作一遍测试,但这个工作可能并不适合他们,因为他们不熟悉程序的边界条件和逻辑。

三、自动测试(Automated Testing)

自动测试解决了手工测试的不足。因此,要回答“除了软件开发中一直在做的那些事情外,测试驱动的开发到底意味着什么?”这个问题,测试驱动的开发在整个开发过程中引入自动测试,并不断改进这些测试以适应程序代码的扩展。注意这中间的两个要点,第一,测试是自动的表示它不但可以重复进行,还要很方便移植,可重复是指你可以一遍又一遍的对同样的代码进行测试,并且每次都得到同样的结果;方便移植是指别人可以使用你的测试,自己来验证你的程序是否能通过这些测试。第二点要求测试的改进包含到程序本身代码的改进中,测试落后于代码可不是件好事,因为这样测试就不能真正的验证程序功能正确与否,因此测试代码需要与应用本身的代码保持同步前进。

软件的自动测试有两个主要方法。第一个是通过可重现的recorded macros。Mercury Interactive (http://www.mercuryinteractive.com/products/winrunner/) 的WinRunner等就是用的这种方法。尽管很容易建立,但宏是很不稳定的,需要经常修改,因为它们通常依赖于按钮和组件的物理位置,而不是在parsed document中的结构位置。对开发人员来说,测试框架不管是产生不正确的错误信息,还是需要大量的工作才能保持和代码同步,都是非常痛苦的事情,

第二个进行web应用自动测试的主要方法就是通过编程API。这样你就有了测试框架,软件库可以检查条件是否满足,报告错误的数量和类型,你可以在测试代码中调用这个框架。你的测试代码就是继承自测试框架的一套类,它们从应用代码中初始化对象,调用方法来验证给定输入会得到预期的结果。采用编程API方案的包括JUnit、HttpUnit、各种单元测试和黑盒web测试的工具等等。这种方案非常灵活,大多数情况下它大大减少了测试代码的维护时间,并且使应用中的复杂功能测试成为可能,尤其是服务器端应用。这种测试可交替的调用programmatic testing,API-driven testing,或者programmatic API-driven testing。

相对于recorded macros模式来说,基于API的自动测试方法的第一个弱点是它的需要更长创建时间。当你的问题是鼠标移动和点击时很难减少设置时间。第二个问题是绝大多数客户不会写测试程序的。客户了解的是业务过程,而不是技术,客户可能觉得移动鼠标和点击鼠标容易得多,这一点非常重要,如果你想让客户在开发过程中就参与进来的话,客户参与是极限编程的鼓吹者推荐的方法。

虽然如此,基于API的方法在许多方面存在着优越性,可以在大多数应用中使用,因为应用程序随时间改变的程序非常大,所以花在测试程序维护上的时间比测试程序的创建时间占的比例更大。而且recorded macro方法有一个致命限制:只有在应用代码写完之后你才能建立测试。如果你们的开发习惯是不在程序完成前测试,那非常好。否则,如果你坚持测试先行的习惯,正如作者推荐的,那非常不幸recorded macros不适合你,因为在记录宏进行重放之前,你必须有一个能运行的应用程序。

好的方案可能既有recorded macros测试,又有基于API的程序化测试。通过鼠标拖点式测试你可以让最终用户参与到测试中来,保证你的程序满足业务需求。通过程序化测试则可以在技术角度确保程序组件按预计的情况工作。

使用程序化测试,你有两种选择:功能测试和单元测试。功能测试也叫做黑盒测试,是指在不知道(或者忽略)内部实现的情况下,在一个较高的层次上进行测试。功能测试用于验证程序是否完成业务需求,它模拟采用与最终用户一致的方式与程序进行交互。最终用户可能是使用基于web应用的业务人员,也可能是通过你所提供的API来使用你的类库的开发人员。如果你一定要纠缠概念,功能测试和黑盒测试还是有一些区别的,因为技术上功能测试可以在容器内进行(如果要测得是web程序的话),但实际上绝大多数功能测试是在黑盒中做,除了公布的接口外你一无所知。相反,单元测试包括底层代码的验证,为了对确保内部组件没有问题,必须了解程序的内部结构。你还需要知道那些类和方法的实现。如果要测的程序是给开发人员使用的软件库,你的单元测试包括所有重要的类和方法,甚至在发布给用户的API文档中没有列出的内容。功能测试与程序交互的方式是通过点击按钮和信息入口,进入程序可见的forms。单元测试与程序的交互是通过Java方法调用来访问。

对前面提到的手工测试的四个缺陷,自动测试都给出了很好的解决:

排除重复。用自动测试你可以把这项工作交给计算机去办,它们将严格按照你每次测试的流程进行操作。

减少错误。计算机每次都用同样的方式执行重复操作。而且,因为测试是cumulative,而且容易调用,即使某一些代码修改在直接影响范围之外很远的地方造成破坏,你仍然能找到这个错误,因为你以前写的检查那些(被破坏的部分)代码的测试会告诉你。自动测试的cumulative是一个非常有用的特点,也是相对手工测试来说一个重要的优点。用手工测试来检查程序的其它部分是不切实际的。

允许组件的独立测试。自动测试,至少其中的基于API的程序化方式,可以让你测试程序的不可视部分,例如你认为是程序核心的业务逻辑。编程测试在其它程序(测试代码)中调用你的代码。粒度由你自己把握。如果不考虑程序的任何内部实现,你可以模拟一个web浏览器或者使用鼠标操作桌面的用户,进行黑盒或功能测试。你也可以调用类库德公共API,因为你不考虑API内部的实现,这也可以算作一种黑盒或功能测试。你还可以调用隐藏类或公共类的内部方法,来验证它们按你想的方式工作,因为针对的是独立的方法和类,而且你会知道算法等内部实现细节,这就是单元测试。你要做什么测试,就选择什么级别的细节。

让最终用户自己来验证软件的功能正确。它的作用不仅在于防止破坏用户数据的bug,而且可以帮助发现你的软件在新的环境中可能出现的无法预料的问题。新的环境包括不同的,新的硬件,不同的网络配置,或者各种各样的其它差异。自动测试可以把你的测试转移到其它平台上,确保你的程序在任何地方都能正确运行。

四、工具(Tools)

下面的几个工具对自动测试非常有用:

JUnit。JUnit是单元测试的鼻祖。其它工具经常在JUnit基础上创建,因为JUnit提供了单元测试和功能测试都必须用到的两项功能:断言检查和结果报告。可以在www.junit.org找到JUnit.

HttpUnit。HttpUnit是在JUnit之上构建的测试框架,它支持web应用的黑盒测试和in-container测试。它是功能,你可以用它验证软件符合业务需求,在可视的级别符合预期行为。有趣的是,HttpUnit的基础代码实际上跟测试没什么关系。HttpUnit库的目的是加强HTTP对web应用的访问,它支持的特征包括状态管理(cookies)、请求提交、应答解析(解析),以及网络蜘蛛(web spider)工具包需要的一些特征。HttpUnit还有一个支持容器内测试的类ServletUnit。在JUnit提供的断言功能和结果报告功能的基础上,HttpUnit成为了一个测试web应用非常有用的工具。可以在www.httpunit.org找到HttpUnit。

jUnit. jWebUnit是在HttpUnit上创建的一个辅助工具包,它减少测试web程序需要你写的代码。简单的说,你可以把它当作HttpUnit的宏程序库,提供到HttpUnit代码段的快捷方式,简化web程序测试中的大多数行为。HttpUnit提供相对底层的接口让你自己定制许多事情。你可能觉得jWebUnit有用,也可能不,如果用HttpUnit可以解决你的所有问题,jWebUnit也可以。它可能带来更多的代码,但你有更好的控制。可以在http://jwebunit.sourceforge.net/找到jWebUnit。

StrutsTestCase. StrutsTestCase是为测试Struts应用在JUnit基础上创建的测试框架。Struts是用Java开发web应用的程序员非常喜欢的模型-试图-控制器(MVC)平台,它简化了数据、表示和逻辑分离的易维护性组件式代码开发。Struts使web程序容器间(in-container)的功能测试和单元测试变得复杂,因为它们夹在servlet容器和你的程序之间。这就意味着这个测试框架要认识Struts,能处理Struts的容器间测试。由于不需要知道web程序的内部实现,HttpUnit的黑盒测试仍然工作得很好。然而你无法用HttpUnit做Struts应用的容器间测试,因为HttpUnit要独立的位于你的程序和servlet容器之间。StrutsTestCase是专为Struts程序的的容器间测试设计的。StrutsTestCase可以在http://strutstestcase.sourceforge.net/获得。

五、总结(Conclusion)

如果你同意测试是重要的,而不仅仅是需要的,那么你就会开始自动化你的测试,或者继续已经使用的自动测试。如果你同意自动测试比手工测试优越,那么在开发程序代码的同时发展自动测试就是合理的。维护测试代码只是增加了一点点负担,但它使你不必担心程序是否满足测试代码的需求。采用了自动测试和程序代码修改的同时维护测试代码这两个原则,你就是采用了测试驱动的开发。程序的可靠性和质量都会得到提高,你的客户,不管是外面的零售客户,还是其他部门的同事,可能会感谢你,也可能更少的因为软件不稳定打扰你,或者两都都有。最后,写测试代码比反复的用鼠标点来点去的测试软件有趣得多。

现在准备阅读测试驱动的开发系列的下一篇论文。这篇标题为“业务层测试驱动的开发”(“Test-driven Development for the Business Tier”)的论文将探讨服务器端组件测试的细节,包括EJB和普通的Java类。


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织