UML软件工程组织

 

 

使用配置文件定义 Mock 对象,创建高效、灵活的测试用例

2008-05-30 作者:郑 闽睿,黄 湘平 来源:IBM

 
本文内容包括:
本文主要讨论如何利用配置文件对 Mock 对象以及它的行为进行描述,从而分离测试数据和代码,创建高效、灵活的测试用例。同时,本文给出了一套基于开源项目 EasyMock 的实现,并通过一个示例来说明如何利用这一实现编写测试用例。

使用 Mock 方法能够模拟协同模块或领域对象,从而把测试与测试边界以外的对象隔离开。使单元测试顺利进行。然而,Mock 方法在辅助测试的同时,也给开发或测试人员带来额外的编码工作。另外,由于 Mock 对象本身并不能对测试数据进行管理,因此测试数据的变动和 Mock 对象本身的变动,可能就会极大的增加编译和部署的时间。

本文提出一种利用 XML 文件对 Mock 对象进行配置的机制,并在开源项目 EasyMock 的基础上实现了这种机制。实际上,读者可以基于任何的自己熟悉的 xMock 项目来实现这里的思想。

1.Mock对象的创建方法

开发和测试人员在利用 Mock 方法进行单元测试时发现,编写自定义 Mock 对象会带来大量额外的编码工作:如果为测试中用到的每一个协同模块或领域对象手动编写 Mock 对象,最终的结果将是 Mock 对象的数目随着系统中实际对象数目的增长而增长。此外,这些为创建 Mock 对象而编写的代码也很有可能引入错误。

目前,由许多开源项目对动态构建 Mock 对象提供了支持,这些项目能够根据现有的接口或类动态生成 Mock 对象,从而避免了编写自定义的 Mock 对象,这样不仅能减少一定的编码工作,也可以降低错误引入的可能。

EasyMock 就是这些开源框架中的一个,它是一套通过简单的方法对于给定的接口生成 Mock 对象的类库。它提供对接口的模拟,能够通过录制、回放、检查三个步骤来完成大体的测试过程。EasyMock 可以验证方法的调用种类、次数和顺序,可以令 Mock 对象返回指定的值或抛出指定异常。通过 EasyMock,开发或测试人员能够比较方便的创建 Mock 对象,在一定程度上减少了创建 Mock 对象所带来的工作量。

2.EasyMock 使用示例

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 中的参数常常会在开发过程中发生改变,这会影响大量测试代码);
  • 单元测试的测试过程包含在测试代码中,当测试用例发生变化,测试代码有可能需要全部重写,造成代码的频繁修改和引入错误的机会。

3.利用 XML 文件配置 Mock 对象

为了改进目前 EasyMock 使用方法中存在的不足,我们需要引入配置文件来对 Mock 对象进行定义。我们的目标是通过配置文件的使用来实现测试代码和数据的分离。当开发人员因为测试用例的变化而需要改变 Mock 对象的测试行为时,就可以直接对配置文件作出改动,而无需修改测试代码。

构建 Mock 对象需要以下两方面的信息:(1)Mock 对象对应的接口或类信息;(2)Mock 对象的预期行为与输出。如果将以上两类信息配置在文件中,通过对配置文件的解析来构造 Mock 对象,就可以实现测试代码和数据分离的目标,从而改进现有 Mock 对象构造方法中的不足。

本文在提出使用配置文件定义 Mock 对象这一机制的同时,也提供了一个基于 EasyMock 的实现。我们将这一实现称为 XMLEasyMock。XMLEasyMock 的完整实现和相关的测试代码都可以在 xmleasymock.zip 中找到。如果您使用 Eclipse 作为 IDE,那么您可以将它导入您的 Workspace(如下图):

图1:导入 xmleasymock.zip 后的 workspace
导入 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 对象生成相关类图
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 对象预期行为相关类图
设定 Mock 对象预期行为相关类图

XMLEasyMock 对 Mock 对象预期方法是通过类反射机制进行调用的。如图4所示,当 MockBehavior 的 runMockMethod 方法被调用时,它首先通过 Mock 对象名查询 Mock 对象,接着从 ParamValue 中取出用户设定的参数类型和参数值。根据 Mock 对象的类型、Mock 对象的方法名和参数类型列表,我们可以通过 Class 的 getDeclaredMethod 获取到对应的 Method 对象。最后,runModkMethod 方法调用 Method 对象的 invoke 方法,完成 Mock 对象预期方法的调用。

图4:Mock 对象预期方法调用时序图
Mock 对象预期方法调用时序图

在对预期方法进行调用之后,我们需要通过 EasyMock 类对方法的预期输出进行设定。我们以设定预期返回值为例进行说明(设定预期异常抛出与此类似)。

图5:设定 Mock 对象预期输出时序图
设定 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 对象和设定预期行为时序图
构建 Mock 对象和设定预期行为时序图

EasyMockUtil 的 loadConfig 方法 MockObjectController 的 loadMockObjects 方法和 MockBehaviorController 的 loadMockBehaviors 方法读取和创建 Mock 对象及其预期行为。MockBehaviorController 的 runMockBehaviors 先后调用 runMockMethod 和 runCtrlOptions 方法设定 Mock 对象的预期方法调用和预期输出。最后,loadConfig 方法调用 MockObjectController replay 方法将 Mock 对象切换成 Replay 状态。

4.利用 Mock 对象定义机制配置预期结果

在进行单元测试时,被测试模块的预期结果也编码在代码中,当测试数据或是测试用例发生变化时,预期结果也将发生改变。我们是否能将预期结果也定义在配置文件中呢?

我们可以将被测试的对象在正确运行的情况下的行为抽象为一个 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>

5.使用配置文件运行测试用例

与在代码中动态构建 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 对象。最后,我们将实际结果和预期结果进行比对,从而完成测试。

6.结论

我们通过配置文件对 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/ 上找到。
讨论
 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号