测试开发

《JUnit实战》第8章 容器内测试(续2)

8.4 比较stub、mock objects和容器内测试

在这一小节中,我们将比较*上文已介绍的用来测试组件的不同方法。这一小节抽取了许多来自论坛和邮件的关于stub、mock objects和容器内测试优缺点的问题。

8.4.1 stub的优点与缺点

在第6章中,我们已经介绍了stub技术,作为第一种容器外测试技术。为了测试和断言一个指定的类的实例状态,stub可以很好地隔离这个类。例如,替换一个 Servlet容器可以让我们跟踪已经产生了多少请求、服务器的状态是什么样的,或者被请求的URL是什么。

然而,使用了mock objects,我们就能够测试服务器的状态及其行为。当使用mockobjects 技术时,我们需要编写代码并证实了预期,我们检查每一个步骤来查看测试是否执行了领域方法,以及测试调用了这些方法多少次。

比起mock objects,stub最大的优势之一就是更易于理解。相较于mock objects需要一个完整的框架才能运行,stub只需用少量的额外代码就可以隔离一个类。stub的缺点在于它们依赖于外部工具和补丁,并且它们也不跟踪被模拟对象的状态。

回顾第6章的内容,我们很容易使用stub来模拟了一个 Servlet容器;而使用mockobjects这样做的话就会非常困难,因为我们需要模拟出容器对象的状态和行为。以下总结了 stub的优点和缺点。

优点:

它们是快速和轻量级的;

它们易于编写和理解;

它们功能强大;

测试是粗粒度的。

缺点:

需要特定的方法来验证状态;

它们不测试模拟对象的行为;

对于复杂的交互,它们非常耗时;

当代码发生变化,它们需要更多的维护。

8.4.2 mock objects的优点和缺点

比起容器内测试,mock objects'最大的优势是mock objects不需要一个运行的容器来执行测试。使用mock objects可以快速建立起测试,而且测试运行速度也很快。其主要的缺点是,被测试的组件并不是在要部署它们的容器中运行。这些测试不能检查组件和容器之间的交互。当组件运行在容器内时,这些测试也无法对组件之间的交互进行测试。

你仍然需要借助一种方法来执行集成测试。通过编写和运行功能测试可以做到这一点。功能测试的问题在于,它们是粗粒度测试,只能测试一个完整的用例——这样你就无法享受到细粒度单元测试带来的好处。你也不能使用功能测试像使用单元测试那样测试许多不同的情况。

mock objects还有一些其他方面的缺点。例如,你可能需要创建大量的mock objects,

而创建这些对象已被证实会带来相当大的开销。显而易见,代码越简洁(使用小且专注的方法),建立测试也就越容易。

mock objects另一个重要的缺点是,为了建立一个测试,你往往需要明确知道被模拟的API的行为是怎么样的。要知道你自己的API行为并非难事,但是要了解另一个API,比如 Servlet API,就没有那么容易了。

尽管一种指定类型的各种容器都实现了相同的API,但所有容器的行为并不是一致的。例如,来看一下以下的Servlet方法:

publie void doGet 《HttpservletRequest request,

    HttoservletResponse response) {

     responSe,SetContentType("text/xml")

}

这个示例看上去非常简单,但是如果你在 Tomcat (http://tomcat.apache.org/)和 1.6.0之前版本的Orion (http://www.orionserver.com/)下运行这个例子,你就会注意到行为的不同。在 Tomcat下,被返回的内容类型是textxml;而在Orion下,被返回的内容类型是 text/html。

这是一个极端的例子;所有的Servlet容器都设法尽量以相同的方式来实现API。但是,对于不同的Java EE API来说,情况就要糟糕很多,尤其对于EJB API而言。执行者可能对一个 API规约有不同的解释,并且规约本身甚至都会自相矛盾,使得执行规约非常困难。此外,容器也会有bug;上文所展示的示例可能就是Orion 1.6.0中的一个bug,虽然这是不幸的,但是,为了在任何项目中使用各种第三方库,你将不得不与 bug、技巧打交道。

在本小节的结尾部分,我们来总结一下使用mock objects进行单元测试的缺点:

这个方法不测试容器与组件或者组件之间的交互,它也不测试组件的部署情况;

它要求对模拟的API极其熟悉,而这可能很难达到(尤其对外部库而言);它是更加细粒度的,这可能使你被大量的接口所淹没;

像stub一样,在代码发生变化时它也需要维护。

8.4.3 容器内测试的优点与缺点

到目前为止,我们已经介绍了容器内单元测试的优点。但是,容器内测试也存在一些缺点,我们现在来讨论一下。

需要特定的工具

mock objects一个主要的缺点是,虽然其概念是普通适用的,但是对于要被测试的API而言,实现容器内单元测试的工具确实特定的,如用于Java EE测试的Apache Jakarta Cactus。如果你希望为另一个组件模型编写集成单元测试,那么取决于是否存在这样一个框架。由于mock objects 的概念是普遍适用的,所以你几乎可以使用mock objects测试任何APl。

没有良好的IDE支持

大多数容器内测试框架的一个重要缺点是缺乏良好的IDE集成。在多数情况下,你能够使用Ant或 Maven执行测试,这也提供了在持续集成服务器(Continuous IntegrationServer, ClS,参见第11章)上运行一个构建的能力。或者,IDE 可以执行使用了mockobjects的测试,就像执行普通的JUnit测试那样。

我们坚定地认为,容器内测试属于集成测试的一种。这意味着,你不需要像普通单元测试那样频繁地执行容器内测试,并且极有可能在一个持续集成服务器上运行这些容器内测试,从而减轻对IDE集成的需求。

更长的执行时间

另一个缺点是性能。为了让测试运行在容器中,你需要启动和管理这个容器,这可能是很耗费时间的。要花费多少时间(和内存),这取决于容器:Jetty可以在1秒之内启动,Tomcat可以在5秒内启动,而 WebLogic可能需要30秒。并不是只有容器才会有启动延迟。例如,如果单元测试遇到了一个数据库,那么在开始测试之前,该数据库必须处于一种预期的状态(关于数据库应用测试,可以参考第17章)。在执行时间方面,集成单元测试要比mock objects耗时更长。因此,你就不可能像运行逻辑单元测试那样频繁地执行集成单元测试。

复杂的配置

容器内测试的最大缺点是测试的配置很复杂。因为应用程序及其测试都运行在一个容器内,所以应用程序必须被打包(通常打包成一个WAR或EAR文件)和部署到容器中。然后你必须启动容器并运行测试。

另一方面,因为为了实际生产你必须执行这些完全相同的任务,最好的做法之一是自动化这个过程,将其作为构建的一部分,并为了测试目的重用它。作为JavaEE项目最复杂的任务之一,提供打包和部署的自动化形成了双赢的局面。提供容器内测试的需求将会驱使我们在项目初期就创建这个初始化过程,这也将促进持续集成。

为了达到这一目的,大多数容器内测试框架都包含了对诸如 Ant 和 Maven此类的构建工具的支持。这将有助于隐藏复杂性,包括构建各种运行期间的工件、运行测试和采集报告。

8.4.4 容器内测试与容器外测试

标准设计的一个目标是将表现与业务层分离开来。例如,为了一个从数据库检索客户列表的标签,你应该使用两个类来实现代码。一个类实现这个标签并依赖于Taglib API,而另一个类实现数据库访问并且不依赖于Taglib API但依赖于数据库类,如JDBC。

分散关注点(Separation-of-Concerns)策略不仅允许在多个上下文中重用类,而且也简化了测试。你可以使用JUnit和mock object 来测试业务逻辑类,也可以使用容器内测试来验证标签类。

比起 mock objects,虽然容器内测试需要更多的设置,但是这些投入是非常物有所值的。你可能不会经常运行容器内测试,但是这些测试能够确保你的标签可以在目标环境中正常工作。虽然你可能会想要跳过一个组件(如 taglib)的测试,但是由于功能测试最终将测试该标签作为一项副作用,所以我们建议你打消这种想法。所有的组件都得益于单元测试。在这里我们列出这些好处:

细粒度测试能够反复运行,并且能够告诉你代码何时、何地以及为何失败了。

你可以对组件进行全面的测试,不仅针对正常的行为,而且也可以针对错误的条件。例如,在测试一个标签访问数据库时,你应该确认当与数据库的连接断开时该标签的行为是否会与预期的一致。这在自动化的自动测试中将很难测试,但是当你组合使用容器内测试和mock objects时,它就会变得很容易测试。

8.5 小结

我们已经知道,一旦要对容器内的应用程序进行单元测试时,采用标准的 JUnit 测试就显得力不从心了。虽然使用mock objects能够完成这类测试,但是它遗漏了一些情况,如检查容器内组件之间的交互和组件与容器的交互的集成测试。为了检查容器内的行为,我们就需要采用一种技术来进行构架方面的测试:容器内测试。

虽然容器内测试很复杂,但是它解决了以上这些问题,并为开发者提供了必要的信心来修改和开发他们的应用程序。本章是阅读本书最后一部分的知识基础,在本书的最后一部分中我们将会继续探索这样的测试框架。

接下来,我们开始介绍本书的第3部分。在这一部分中,我们会基于测试驱动开发的原则,将JUnit集成到构建过程中。

相关内容

文章评论

表情

共 0 条评论,查看全部
  • 这篇文章还没有收到评论,赶紧来抢沙发吧~