本文主要讨论如何利用配置文件对 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 对象配置文件的模板:
清单3:Mock 对象配置模板
<?xml version="1.0" encoding="UTF-8"? >
<mockConfig>
<mockObjects>
<mockObject name="Object name" mockedClass="Mock class or interface" />
......
</mockObjects>
<mockBehaviors>
<mockBehavior mockObject=" Object name" method="Expected invocation method">
<paramValues>
<paramValue type="Parameter type" value="Parameter value" />
</paramValues>
<ctrlOptions>
<ctrlOption option="Control option" value="Expected return value"
times="Expected invocation times" />
……
</ctrlOptions>
</ mockBehavior>
......
</ mockBehaviors>
</mockConfig> |
其中,<mockObjects> 部分将配置 Mock 对象的生成信息,<mockBehaviors>
部分将配置 Mock 对象的预期行为和输出。接下来,我们将对这两部分进行详细的说明。
根据配置文件生成 Mock 对象
配置文件中所包含的 Mock 对象生成信息包含在 <mockObject> 元素当中。<mockObject>
元素包含两个属性 name 和 mockedClass,分别对应 Mock 对象的名称和对应的接口或类。Mock 对象的名称用于和配置文件中的其它部分相关联,而对应的接口和类用于
Mock 对象的生成。
ResultSet 接口是每个 Java 开发人员都非常熟悉的接口。以 java.sql.ResultSet 接口为例,为其生成一个
Mock 对象 mockResultSet,可以在文件中配置为:
<mockObject name="mockResultSet" mockedClass="java.sql.ResultSet"
/>
我们可以设想一下,在 EasyMock 中,如果我们需要创建 ResultSet 接口的一个 Mock 对象,这个过程应当是:
IMocksControl mocksControl = EasyMock.createControl();
ResultSet mockResultSet = control.createMock(ResultSet.class);
其中,IMocksControl 接口的实例 mocksControl 能生成并管理多个 Mock 对象。在 XMLEasyMock
中,我们为每个 Mock 对象创建一个 MockObject 类的对象,同时用一个 MockObjectController 对象来管理这些
Mock 对象。MockObjectController 类拥有一个 IMocksControl 成员变量,同时提供了 replay、verify
和 reset 方法,供外部调用(如下图):
图2:Mock 对象生成相关类图
EasyMockUtil 是提供给外部程序调用的工具类,loadConfig 方法用于读取配置文件,findMockObjectByName
方法可以通过 Mock 对象的变量名返回 Mock 对象。
配置 Mock 对象的预期行为
接下来我们需要配置的是 Mock 对象的预期行为。Mock 对象的预期行为可以简单的理解为是 Mock 对象方法的调用以及该方法的预期输出。我们需要在文件中分别配置方法的预期调用和预期输出。
Mock 对象的预期方法调用配置在 <mockBehavior> 元素中。每个 <mockBehavior>
元素都包含两个属性:mockObject 和 method 属性。mockObject 指定该行为对应的 Mock 对象的名称(Mock
对象必须在 <mockObject> 中定义过),method 属性则指定Mock对象中预期调用的方法。<mockBehavior>
的子元素<paramValues>包含了需要配置的方法所对应的参数列表。<paramValues>的每个子元素<paramValue>都包含两个属性:type和value,分别指定了参数类型和参数值。
我们以 ResultSet 接口的 Mock 对象 mockResultSet 为例,如果我们期望对 getString 方法进行调用,可以配置以下信息:
清单4:Mock 对象生成信息
<mockBehavior mockObject="mockResultSet" method="getString">
<paramValues>
<paramValue type="int" value="1" />
</paramValues>
......
</mockBehavior> |
在对 Mock 对象的方法调用进行配置以后,我们接下来对方法的预期输出进行配置。方法的预期输出定义包含在 <ctrlOptions>
中。<ctrlOption> 中的 option 属性指定了 MockControl 对象在指定方法返回值时选用的选项。Option
属性可选的值包括:
- andReturn
- andStubReturn
- andThow
- andSubThrow
- andVoidCallable
其中,andReturn 选项用于设定方法的预期返回值,当 option 属性为 andReturn 时,我们可以在 value
属性中配置方法的返回值。在预期方法确定以后,其返回值类型也确定了,因此我们无需在此指定返回值类型。times 属性用于指定预定方法的调用次数。如果希望为
Mock 对象方法设置默认的预期返回值,那么你可以选择 andStubReturn,这时 value 属性中的返回值将作为预期方法的固定返回值,而无需多次设定。
andThrow 选项用于设定预期异常抛出。当 option 属性为 andThrow 时,value 属性用于指定预期的异常类型。times
属性同样用于设定预期异常抛出的次数。如果希望为 Mock 对象方法设定默认的异常抛出,您可以相应的选择 andSubThrow。
如果预期方法的返回值为空(void),那么您应当指定 andVoidCallable 方法。这时 value 属性不用设定(如果设定,XMLEasyMock会忽略该属性)。
我们仍然用 ResultSet 接口的 getString 方法为例,说明预期输出的配置效果:
清单5:Mock对象预期行为定义
<mockBehavior mockObject="mockResultSet" method="getString">
......
<ctrlOptions>
<ctrlOption option="andReturn" value="My return value" times="1" />
<ctrlOption option="andThrow" value="java.sql.SQLException" times="2" />
</ctrlOptions>
</mockBehavior> |
以上的配置相当于在 EasyMock 中调用:
expectLastCall().andReturn("My return value").times(1);
expectLastCall().andThrow(new SQLException()).times(2);
XMLEasyMock 中为每个 Mock 对象的预期行为创建一个 MockBehavior 对象。MockBehavior
类中包含了两个列表,分别包含了多个 ParamValue 对象和 CtrlOption 对象。所有 MockBehavior 对象都由
MockBehaviorController 统一管理。MockBehaviorController 提供了 loadMockBehaviors
和 runMockBehaviors 方法,分别用于读入 MockBehavior 和执行预期行为设定。这些类的关系如下图所示:
图3:设定 Mock 对象预期行为相关类图
XMLEasyMock 对 Mock 对象预期方法是通过类反射机制进行调用的。如图4所示,当 MockBehavior 的 runMockMethod
方法被调用时,它首先通过 Mock 对象名查询 Mock 对象,接着从 ParamValue 中取出用户设定的参数类型和参数值。根据
Mock 对象的类型、Mock 对象的方法名和参数类型列表,我们可以通过 Class 的 getDeclaredMethod 获取到对应的
Method 对象。最后,runModkMethod 方法调用 Method 对象的 invoke 方法,完成 Mock 对象预期方法的调用。
图4:Mock 对象预期方法调用时序图
在对预期方法进行调用之后,我们需要通过 EasyMock 类对方法的预期输出进行设定。我们以设定预期返回值为例进行说明(设定预期异常抛出与此类似)。
图5:设定 Mock 对象预期输出时序图
如图5所示,MockBehavior 类提供了 runCtrlOptions 用于设定方法的预期输出。runCtrlOption
方法首先调用之前得到的 Method 对象的 getReturnType 方法,获取方法的返回值类型,并将该返回值类型作为参数传递给
CtrlOption 的 runCtrlOption 方法。runCtrlOption 方法首先调用 EasyMock 类的 expectLastCall
静态方法,获得 Mock 对象所对应的 IMocksControl 实例,之后,根据预期方法的返回值类型对配置文件中的返回值进行格式化,将格式化后的数据作为参数传递给
IMocksControl 的 andReturn 方法,最后,调用 times 方法设定预期调用次数。
以上是 XMLEasyMock 对 Mock 对象生成、Mock 对象预期行为设定的具体实现。对于外部程序而言,只需要调用 EasyMockUtil
提供的 loadConfig 静态方法就可以达到根据配置文件构建 Mock 对象的目的了:
图6:构建 Mock 对象和设定预期行为时序图
EasyMockUtil 的 loadConfig 方法 MockObjectController 的 loadMockObjects
方法和 MockBehaviorController 的 loadMockBehaviors 方法读取和创建 Mock 对象及其预期行为。MockBehaviorController
的 runMockBehaviors 先后调用 runMockMethod 和 runCtrlOptions 方法设定 Mock
对象的预期方法调用和预期输出。最后,loadConfig 方法调用 MockObjectController replay 方法将
Mock 对象切换成 Replay 状态。
在进行单元测试时,被测试模块的预期结果也编码在代码中,当测试数据或是测试用例发生变化时,预期结果也将发生改变。我们是否能将预期结果也定义在配置文件中呢?
我们可以将被测试的对象在正确运行的情况下的行为抽象为一个 Mock 对象,它的预期输出,就是被测试对象在正确运行情况下的预期输出。通过这种方式,我们就可以用类似配置
Mock 对象的方式对预期结果进行配置了。在 XMLEasyMock 中我们提供了一个测试用的接口 SalesOrder,它的实现类
SalesOrderImpl 的主要功能是从数据库中读取一个 Sales Order 的 Region 和 Total Price,并根据读取的数据计算该
Sales Order 的 Price Level:
清单6:SalesOrder 接口
public interface SalesOrder
{
……
public void loadDataFromDB(ResultSet resultSet) throws SQLException;
public String getPriceLevel();
} |
如果我们对 getPriceLevel 方法进行测试,就可以将该方法在正确运行下的预期输出抽象成 Mock 对象,并配置如下:
清单7:配置预期输出
<mockConfig>
<mockObjects>
<mockObject name="mockSalesOrder" mockedClass="xmleasymock.demo.test.SalesOrder" />
</mockObjects>
<mockBehaviors>
<mockBehavior mockObject="mockSalesOrder" method="getPriceLevel">
<paramValues />
<ctrlOptions>
<ctrlOption option="andReturn" value="expected result 1" times="1" />
<ctrlOption option="andReturn" value="expected result 2" times="1" />
......
</ctrlOptions>
</mockBehavior>
</mockBehaviors>
</mockConfig> |
与在代码中动态构建 Mock 对象不同,XMLEasyMock 是在配置文件的解析过程中动态生成 Mock 对象的。因此,如果用户需要使用
Mock 对象,需要从解析模块中获取。XMLEasyMock 提供了一个工具类 EasyMockUtil,这个工具类提供了 findMockObjectByName
方法用于返回 Mock 对象。该方法的输入参数就是在配置文件的 <mockObject> 元素中配置的 name 属性。另外,我们在上文中提到,EasyMockUtil
提供了方法 loadConfig 用于装入配置文件,该方法的输入参数就是配置文件的路径。
在 XMLEasyMock 提供的测试代码(SalesOrderTestCase.java)中,我们对 ResultSet 接口进行了模拟,从而对
SalesOrder 的 getPriceLevel 进行测试。在完成相关 Mock 对象的配置之后,我们可以通过 XMLEasyMock
提供的功能对测试用例实现如下:
清单8:完整的 TestCase
public class SalesOrderTestCase extends TestCase {
public void testAfterConfig() {
try {
EasyMockUtil.loadConfig("/xmleasymock/demo/properties/mockConfig.xml");
DBUtility mockDBUtility =
(DBUtility) EasyMockUtil.findMockObjectByName("mockDBUtility");
Connection conn = mockDBUtility.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from sales_order");
SalesOrder realOrder = new SalesOrderImpl();
SalesOrder mockOrder =
(SalesOrder) EasyMockUtil.findMockObjectByName("mockSalesOrder");
while (rs.next()) {
realOrder.loadDataFromDB(rs);
assertEquals(realOrder.getPriceLevel(), mockOrder.getPriceLevel());
}
EasyMockUtil.verify();
} catch (Exception e) {
e.printStackTrace();
}
}
} |
通常,在数据库连接模块中,开发人员都会开发一个类似于 DBUtitlity 的实现来获取数据库连接以及释放数据库资源。在我们的测试用例中,这不是必需的,但为了对真实的开发和测试环境进行模拟,我们也在
mockConfig.xml 中对 DBUtiltiy, Connection 和 Statement 等接口进行了配置。我们通过
DBUtility Mock 对象的名称 "mockDBUtility" 获得 mockDBUtility
对象,并通过它得到 ResultSet 的 Mock 对象。在对 getPriceLevel 方法进行测试时,我们将预期结果抽象成
Mock 对象,它在配置文件中的名称是 mockSalesOrder。我们通过这个名称获得预期结果的 Mock 对象。最后,我们将实际结果和预期结果进行比对,从而完成测试。
我们通过配置文件对 Mock 对象进行定义,实现了测试数据和代码的分离,从而避免了将数据编码在代码中所带来的一系列不便。当测试数据或是测试用例发生变化时,开发或部署人员只需对配置文件作出改动,而不用修改测试代码和重新编译、部署,降低了测试用例发生变化所带来的工作量和时间花销。本文基于
EasyMock 实现了通过配置文件定义Mock对象的机制。实际上,读者可以基于任何的自己熟悉的 xMock 项目来实现这里的思想。
描述 |
名字 |
大小 |
下载方法 |
本文用到的示例代码 |
xmleasymock.zip |
210KB |
|
学习
- EasyMock 的使用方法和原理的详细说明请参见:EasyMock使用方法和原理剖析。
- 您可以在 JUnit 的主页上找到完整的文档和相关下载:http://www.junit.org/index.htm。
- 如果您想要获得 EasyMock 完整的文档和 API,您可以访问 EasyMock 的主页:http://www.easymock.org/。
获得产品和技术
- 在 Source Forge 上,你可以下载到最新的 EasyMock 相关代码:http://sourceforge.net/project/showfiles.php?group_id=82958。
- Eclipse 的相关下载可以在 http://www.eclipse.org/ 上找到。
讨论 |