一、前言
在测试驱动开发过程中我们最关注的是如下一些内容:
目标专一的测试:理想情况下每个测试只有一条断言;
彼此独立的测试:对于每个测试都存在预设环境(Fixture)的建立和清除,以便让测试能够以任意顺序执行;
运行速度的测试:你想能够频繁地运行这些测试。
以 上目标自然导致一种潜在的矛盾。因为简短而专一的测试就意味着你将会有许多这样的测试,从而保证每个都非常简短而专一。而要想使这些测试彼此独立,显然就 需要针对每个测试都有整洁的预设环境。此外,上面我们最后一个目标即是:希望测试速度执行神速......尽可能地快......以便我们相当频繁地执行 它们(因为我们现在在做测试编程,而还是根本目标--最终的目标编程)。
对于中小型目标来说,以上预设环境的建立似乎不成问题;但是,在开发大型项目时,如果测试的预设环境变得相当复杂而且构造或清除起来非常困难时,我们该怎么办呢?
针 对以上问题,有些开发人员可能想到创建大量的预设环境代码,而有些开发人员所在的工作环境可能是一种大型的、复杂的系统--这种环境可能是数据库、工作流 系统或者是某种你正在为其开发扩展模块的系统。为此,你的预设环境代码需要使系统进入某种特定的状态,以便按照测试所需要的方式进行响应,这种工作不大可 能很快就完成。
如果存在以上难以处理的测试资源的话,我们如何在专一性、独立性和速度这三个目标之间进行权衡呢?
实践证明:模拟对象(即Mocks)是一种极其有效可靠的解决方案。当我们很难或不可能为某种难以处理的资源创建需要的状态或访问那种资源受到限制时,就可以充分发挥模拟对象的作用。其实,模拟对象还有其他重要作用。在本篇中,我们将详细探讨什么是模拟对象及其可能发挥的有关作用。
二、什么是模拟对象(Mocks)
根据测试驱动开发权威人士的解释,模拟对象(即Mocks)用来取代真实对象的位置,用于测试一些与真实对象进行交互可依赖于真实对象的功能。模拟对象(即Mocks)背后的基本思想是创建轻量级的、可控制的对象来代替为了编写测试而需要使用的对象。此外,模拟对象(即Mocks)还能够让你指定和测试你的代码与模拟对象(即Mocks)本身之间的交互。
三、模拟对象的用途总结
除了可以保持测试预设环境的轻量级,以便可以快速地创建和清除外,使用模拟对象还有其他一些好处。我们使用模拟对象的原因相当之多。具体说来,有以下诸多理由要求我们采用模拟对象的辅助解决方案:
(1)有助于保持设计的松耦合性
采用模拟对象有助于我们强化以接口为中心的设计。针对模拟对象进行编程可以消除依赖对象内部实现的可能性。
(2)检查你的代码使用另一个对象是否得当
通过在模拟对象中设定期望值(Expectation),我们可以验证编写的代码是否恰当地使用了模拟的接口。
(3)由内而外地利用测试来驱动代码的开发
通过在模拟对象中设定返回值,可以为我们正在开发的代码提供特定的信息,然后测试最终的行为是否正确。
(4)可以让测试运行得更快
通过模拟诸如通信或数据库这样的子系统,我们能够避免设置和清除连接等资源所带来的开销。
(5)可以让我们更容易地开发与硬件设备、远程系统以及其他一些难以处理的资源进行交互的代码
如果我们所编写的代码需要与一些难以处理的资源进行交互的话,那么我们可以创建一个代理层来隔离实际的资源。这样,我们就能够使用代理层的模拟对象,无需访问实际的设备或系统就可以开发代码。
(6)推迟必需的类实现
针对那些尚未编写出来的但是我们正在开发的代码而又需要与之进行交互的类来说,我们可以使用它们的模拟对象。这样,我们就能够推迟实现这些类,以便集中精 力处理这些类与我们正在编写的代码之间的接口。从而,我们可以针对具体实现作出决定,直到我们了解了更多的信息以后再作决定。如果你的测试所需要的只是模 拟行为,那么使用模拟对象就足够了。
(7)可以让我们在进行测试驱动开发时把要开发的部件与系统的其他部分分隔开来
通过模拟我们正在编写的代码必须要与之交互的部件,我们可以单独专注于这个部件的开发。这样以来,我们的开发速度就会更快,因为我们正在开发的部件与其他部件之间的复杂的交互完全处于我们的控制之下。
(8)提倡基于接口的设计
注意,当你使用以接口为主(或指导思想)的编程理念时才易于使用模拟对象。
(9)鼓励使用组合而不是继承进行程序设计
在OOP设计思想影响下,大多数人总会过度地使用继承技术。结果是,基于继承层次而构建出一种一体化的功能。一个类永远是它所继承的那种样子。于是导致: 模拟位于继承层次中的类的任何一个方面都变得相当困难,因为这个类还要承载它所继承的所有的负担。这种想要对特定方面(例如持久性存储)实施模拟的愿望使 我们倾向于编写出相对较小的类,这些类通过与其他类的相互协作而能够实现丰富的功能。这种类的不同实现,包括模拟对象在内,都能够很容易地相互替换。
(10)改进接口
使用你最终需要实现的类的模拟,可以让你早早地有机会思考接口并改进之。这在使用测试优先的设计时尤其如此。你不得不从将要使用这个接口的类的角度来考虑这个接口--因为你开始编写的是一个使用这个接口的真实的测试程序。
(11)用来测试非同寻常的、不大可能出现的例外情况
你可以创建一个模拟对象,让它返回在通常情况下不会返回的返回值,或是创建一个可以在需要的时候抛出异常的模拟对象。这样可以让你很容易地对异常处理进行测试。总之,出现测试的目的,你完全可以编写出一个能够模拟任何难以发生的情况下模拟。
四、常用模拟框架简介
最开始,Mock Object是完全由测试者自己手工撰写的。这样,无可避免的会带来编写测试用例效率低下和测试用例编写困难的弊病,甚至可能会影响XP实践者“测试先行”的激情。此时,各种各样帮助创建Mock Object的工具就应运而生了。
目前,在Java阵营中主要的Mock测试工具有JMock,MockCreator,MockRunner,EasyMock,MockMaker等,在微软的.Net阵营中主要是NMock,.NetMock,Rhino Mocks和Moq等。
(1)MockObjects
我们可以使用四种方式来利用MockObjects编写测试用例:
1、基于MockObjects的框架编写自己的Mock Object实现并与其他人分享;
2、直接使用MockObjects提供的Mock Object进行测试;
3、使用其中包含的DynaMock模块快速获取Mock Object实例;
4、通过它提供的Helpers对象帮助我们快速构建测试环境。
(2)MockMaker
主页为http://mockmaker.sourceforge.net/。目前这个框架似乎发展缓慢,这从它针对的应用平台为Windows 2000, Linux, Java 1.2 & Java 1.4这一点可以看得出。
(3)MockCreator
JAVA平台的模拟对话框架,开源网址为http://mockcreator.sourceforge.net/,最新下载支持为MockCreator-2006-09-07。
(4)EasyMock
手动的构造 Mock 对象会给开发人员带来额外的编码量,而且这些为创建 Mock 对象而编写的代码很有可能引入错误。目前,有许多开源项目对动态构建 Mock 对象提供了支持,这些项目能够根据现有的接口或类动态生成,这样不仅能避免额外的编码工作,同时也降低了引入错误的可能。
EasyMock 是一套用于通过简单的方法对于给定的接口生成 Mock 对象的类库。它提供对接口的模拟,能够通过录制、回放、检查三步来完成大体的测试过程,可以验证方法的调用种类、次数、顺序,可以令 Mock 对象返回指定的值或抛出指定异常。通过 EasyMock,我们可以方便的构造 Mock 对象从而使单元测试顺利进行。
(5)NMock
NMock 是一个基于动态代理的模拟工具,用于 C# 开发。NMock使用了代理模式,这允许类实现一个接口并以代理的方式将其它对象的调用转向。NMock生成的模拟是通过在运行时使用动态代理来实现的, 这允许模拟对象动态的定义,并不需要添加任何附加的类。通常,一个模拟的实现基于被依赖的接口而创建;NMock支持对接口和类的模拟,另外它还支持属性 模拟。
(6)Rhino Mocks和Moq
Rhino Mocks和Moq是.NET平台下的两个新创建的优秀的模拟对象框架,有关于此二者的对比,读者可以参考中文博客“比较Moq和Rhino Mocks两个测试框架”,URL为:http://blog.joycode.com/haacked/archive/2008/03/24/115023.aspx。从许多的博客文章中可以看到,这两个框架在.NET平台下得到了广泛应用,颇受好评。
(7)JMock
JMock利用模拟对象思想来对Java代码进行测试。概括来看,JMock具有以下特点:容易扩展,让你快速简单地定义模拟对象,因此不必打破程序间的关联,让你定义灵活的超越对象之间交互作用而带来测试局限,减少你测试地脆弱性。
篇幅所限,在此仅仅列举上面几种常见的模拟对象框架,而且仅仅作了简单介绍。至于各种框架的具体特征与相应示例展示,请参考各框架的官方网站。
五、小结
从简短的文章介绍,可以看出模拟对象确实能够协助我们实现在本文开始所关注的三个目标,即:目标专一的测试;彼此独立的测试和快速的测试。到目前为止,模拟对象仍然是一种相当年轻的概念,有关的新工具和新技术将不断被开发出来。针对.NET 2.0框架的Moq就是例证之一。
切记:个别情况下,可能还需要通过手工方式创建模拟类(这些可能要用到MockObjects这样的框架)。尽管如此,你仍然能够通过使用像本文所给出的模拟框架这样的工具来创建模拟类的框架代码--你可以基于它们所产生的框架代码进一步增加定制的行为。
最后还要牢记:模拟对象是一种测试驱动开发的非常好的技术,因为有些情况下它实在非常有用,但是不要过度使用或过度依赖于它。它仅仅是软件开发中锦囊中的妙计之一,具体的应用则真正环境而定。