本文主要讨论如何利用配置文件对 Mock 对象以及它的行为进行描述,从而分离测试数据和代码,创建高效、灵活的测试用例。同时,本文给出了一套基于开源项目 EasyMock 的实现,并通过一个示例来说明如何利用这一实现编写测试用例。
使用 Mock 方法能够模拟协同模块或领域对象,从而把测试与测试边界以外的对象隔离开。使单元测试顺利进行。然而,Mock 方法在辅助测试的同时,也给开发或测试人员带来额外的编码工作。另外,由于 Mock 对象本身并不能对测试数据进行管理,因此测试数据的变动和 Mock 对象本身的变动,可能就会极大的增加编译和部署的时间。
本文提出一种利用 XML 文件对 Mock 对象进行配置的机制,并在开源项目 EasyMock 的基础上实现了这种机制。实际上,读者可以基于任何的自己熟悉的 xMock 项目来实现这里的思想。
开发和测试人员在利用 Mock 方法进行单元测试时发现,编写自定义 Mock 对象会带来大量额外的编码工作:如果为测试中用到的每一个协同模块或领域对象手动编写 Mock 对象,最终的结果将是 Mock 对象的数目随着系统中实际对象数目的增长而增长。此外,这些为创建 Mock 对象而编写的代码也很有可能引入错误。
目前,由许多开源项目对动态构建 Mock 对象提供了支持,这些项目能够根据现有的接口或类动态生成 Mock 对象,从而避免了编写自定义的 Mock 对象,这样不仅能减少一定的编码工作,也可以降低错误引入的可能。
EasyMock 就是这些开源框架中的一个,它是一套通过简单的方法对于给定的接口生成 Mock 对象的类库。它提供对接口的模拟,能够通过录制、回放、检查三个步骤来完成大体的测试过程。EasyMock 可以验证方法的调用种类、次数和顺序,可以令 Mock 对象返回指定的值或抛出指定异常。通过 EasyMock,开发或测试人员能够比较方便的创建 Mock 对象,在一定程度上减少了创建 Mock 对象所带来的工作量。
EasyMock 的使用方法和原理的详细说明请参见 "EasyMock 使用方法和原理剖析" 一文。在这里,我们仅以 HttpServletRequest 为例对 EasyMock 的功能做简单说明。
在部署到 Servlet 容器之前,需要和 HttpServletRequest 进行交互的模块可以通过构建 Mock 对象的方式进行单元测试。下面是使用 EasyMock(version 2.3)构建 Mock 对象进行简单测试的例子:
清单1:EasyMock 示例
public class HttpServletRequestUtil {
public static boolean validate(HttpServletRequest request) {
String host = request.getHeader("Host");
return host.startsWith("www.ibm.com");
}
}
public class HttpServletRequestTestCase extends TestCase {
public void testHttpSevletRequest() {
HttpServletRequest mockRequest = createMock(HttpServletRequest.class);
mockRequest.getHeader("Host");
expectLastCall().andReturn("www.ibm.com:80").times(1);
replay(mockRequest);
assertTrue(HttpServletRequestUtil.validate(mockRequest));
verify(mockRequest);
}
}
首先,我们通过 EasyMock 提供的静态方法 createMock 创建 Mock 对象 mockRequest。当 Mock 对象创建好以后,我们就可以对 Mock 对象的预期行为和输出进行设定。对预期行为和输出的设定分成两个部分:(1)对指定方法进行调用;(2)对预期输出进行设定。在上例中,mockRequest.getHeader("Host"); 对 Mock 对象的 getHeader 方法进行了调用,之后用 expectLastCall().andReturn("www.ibm.com:80").times(1) 对Mock对象的预期输出进行了设定。andReturn 方法设定了当 getHeader 方法被调用时,将返回字符串 "www.ibm.com:80",times 方法设定了该方法预期被调用的次数是1。
在结束对 Mock 对象预期行为和方法的设定之后,我们可以调用 replay 静态方法将 mockRequest 对象切换成回放状态。在回放状态下,Mock 对象的方法调用将返回预先设定的输出。在上例中,HttpServletRequestUtil 类的 validate 方法对 mockRequest 的 getHeader 方法进行了调用,并对得到的值进行验证。
最后,我们可以用 verify 方法来验证预期方法的调用是否真的完成了。如果将上例中 expectLastCall().andReturn("www.ibm.com:80").times(1) 设定的调用次数修改为2,而实际测试中只调用了一次该方法,您将会看到以下的错误:
清单2:verify 验证错误
java.lang.AssertionError:
Expectation failure on verify:
getHeader("Host"): expected: 2, actual: 1
at org.easymock.internal.MocksControl.verify
at org.easymock.EasyMock.verify
at org.easymock.demo.testcase.HttpServletRequestTestCase.testHttpSevletRequest
通过示例,我们了解了 EasyMock 的使用方法。EasyMock 能为单元测试提供了一定的便利,然而,它也有一些明显的不足之处:
- 测试数据和预期结果以编码的形式写在测试用例中,测试数据的任何微小变化都会导致代码的重新编译和部署;
- 被测试模块所包含的方法和参数硬编码在测试代码中,方法或参数的变化将导致所有相关测试代码的修改(例如 HttpServletRequest 中的参数常常会在开发过程中发生改变,这会影响大量测试代码);
- 单元测试的测试过程包含在测试代码中,当测试用例发生变化,测试代码有可能需要全部重写,造成代码的频繁修改和引入错误的机会。
为了改进目前 EasyMock 使用方法中存在的不足,我们需要引入配置文件来对 Mock 对象进行定义。我们的目标是通过配置文件的使用来实现测试代码和数据的分离。当开发人员因为测试用例的变化而需要改变 Mock 对象的测试行为时,就可以直接对配置文件作出改动,而无需修改测试代码。
构建 Mock 对象需要以下两方面的信息:(1)Mock 对象对应的接口或类信息;(2)Mock 对象的预期行为与输出。如果将以上两类信息配置在文件中,通过对配置文件的解析来构造 Mock 对象,就可以实现测试代码和数据分离的目标,从而改进现有 Mock 对象构造方法中的不足。
本文在提出使用配置文件定义 Mock 对象这一机制的同时,也提供了一个基于 EasyMock 的实现。我们将这一实现称为 XMLEasyMock。XMLEasyMock 的完整实现和相关的测试代码都可以在 xmleasymock.zip 中找到。如果您使用 Eclipse 作为 IDE,那么您可以将它导入您的 Workspace(如下图):
图1:导入 xmleasymock.zip 后的 workspace
在 XMLEasyMock 中,我们选用 XML 文件作为 Mock 对象的配置文件,XML 文件的自定义和结构特性使得它成为描述 Mock 对象最佳的选择。根据以上对 Mock 对象信息配置的分析,我们可以给出 Mock 对象配置文件的模板: