| 本文主要讨论如何利用配置文件对 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/ 上找到。  |