Mock 方法是单元测试中常见的一种技术,它的主要作用是模拟一些在应用中不容易构造或者比较复杂的对象,从而把测试与测试边界以外的对象隔离开。 编写自定义的 Mock 对象需要额外的编码工作,同时也可能引入错误。EasyMock 提供了根据指定接口动态构建 Mock 对象的方法,避免了手工编写 Mock 对象。本文将向您展示如何使用 EasyMock 进行单元测试,并对 EasyMock 的原理进行分析。 1.Mock 对象与 EasyMock 简介单元测试与 Mock 方法单元测试是对应用中的某一个模块的功能进行验证。在单元测试中,我们常遇到的问题是应用中其它的协同模块尚未开发完成,或者被测试模块需要和一些不容易构造、比较复杂的对象进行交互。另外,由于不能肯定其它模块的正确性,我们也无法确定测试中发现的问题是由哪个模块引起的。 Mock 对象能够模拟其它协同模块的行为,被测试模块通过与 Mock 对象协作,可以获得一个孤立的测试环境。此外,使用 Mock 对象还可以模拟在应用中不容易构造(如 HttpServletRequest 必须在 Servlet 容器中才能构造出来)和比较复杂的对象(如 JDBC 中的 ResultSet 对象),从而使测试顺利进行。 EasyMock 简介手动的构造 Mock 对象会给开发人员带来额外的编码量,而且这些为创建 Mock 对象而编写的代码很有可能引入错误。目前,有许多开源项目对动态构建 Mock 对象提供了支持,这些项目能够根据现有的接口或类动态生成,这样不仅能避免额外的编码工作,同时也降低了引入错误的可能。 EasyMock 是一套用于通过简单的方法对于给定的接口生成 Mock 对象的类库。它提供对接口的模拟,能够通过录制、回放、检查三步来完成大体的测试过程,可以验证方法的调用种类、次数、顺序,可以令 Mock 对象返回指定的值或抛出指定异常。通过 EasyMock,我们可以方便的构造 Mock 对象从而使单元测试顺利进行。 安装 EasyMockEasyMock 是采用 MIT license 的一个开源项目,您可以在 Sourceforge 上下载到相关的 zip 文件。目前您可以下载的 EasyMock 最新版本是2.3,它需要运行在 Java 5.0 平台上。如果您的应用运行在 Java 1.3 或 1.4 平台上,您可以选择 EasyMock1.2。在解压缩 zip 包后,您可以找到 easymock.jar 这个文件。如果您使用 Eclipse 作为 IDE,把 easymock.jar 添加到项目的 Libraries 里就可以使用了(如下图所示)。此外,由于我们的测试用例运行在 JUnit 环境中,因此您还需要 JUnit.jar(版本3.8.1以上)。 图1:Eclipse 项目中的 Libraries2.使用 EasyMock 进行单元测试通过 EasyMock,我们可以为指定的接口动态的创建 Mock 对象,并利用 Mock 对象来模拟协同模块或是领域对象,从而使单元测试顺利进行。这个过程大致可以划分为以下几个步骤:
接下来,我们将对以上的几个步骤逐一进行说明。除了以上的基本步骤外,EasyMock 还对特殊的 Mock 对象类型、特定的参数匹配方式等功能提供了支持,我们将在之后的章节中进行说明。 使用 EasyMock 生成 Mock 对象根据指定的接口或类,EasyMock 能够动态的创建 Mock 对象(EasyMock 默认只支持为接口生成 Mock 对象,如果需要为类生成 Mock 对象,在 EasyMock 的主页上有扩展包可以实现此功能),我们以 清单1:ResultSet 接口public interface java.sql.ResultSet { ...... public abstract java.lang.String getString(int arg0) throws java.sql.SQLException; public abstract double getDouble(int arg0) throws java.sql.SQLException; ...... } 通常,构建一个真实的 我们可以使用 EasyMock 动态构建 ResultSet mockResultSet = createMock(ResultSet.class); 其中 如果需要在相对复杂的测试用例中使用多个 Mock 对象,EasyMock 提供了另外一种生成和管理 Mock 对象的机制: IMocksControl control = EasyMock.createControl(); java.sql.Connection mockConnection = control.createMock(Connection.class); java.sql.Statement mockStatement = control.createMock(Statement.class); java.sql.ResultSet mockResultSet = control.createMock(ResultSet.class);
如果您要模拟的是一个具体类而非接口,那么您需要下载扩展包 EasyMock Class Extension 2.2.2。在对具体类进行模拟时,您只要用 设定 Mock 对象的预期行为和输出在一个完整的测试过程中,一个 Mock 对象将会经历两个状态:Record 状态和 Replay 状态。Mock 对象一经创建,它的状态就被置为 Record。在 Record 状态,用户可以设定 Mock 对象的预期行为和输出,这些对象行为被录制下来,保存在 Mock 对象中。 添加 Mock 对象行为的过程通常可以分为以下3步:
设定预期返回值 Mock 对象的行为可以简单的理解为 Mock 对象方法的调用和方法调用所产生的输出。在 EasyMock 2.3 中,对 Mock 对象行为的添加和设置是通过接口 IExpectationSetters<T> andReturn(T value); 我们仍然用 mockResultSet.getString(1); expectLastCall().andReturn("My return value"); 以上的语句表示 void andStubReturn(Object value); 假设我们创建了 mockStatement.executeQuery("SELECT * FROM sales_order_table"); expectLastCall().andStubReturn(mockResultSet); EasyMock 在对参数值进行匹配时,默认采用 设定预期异常抛出 对象行为的预期输出除了可能是返回值外,还有可能是抛出异常。 IExpectationSetters<T> andThrow(Throwable throwable); 和设定默认返回值类似, void andStubThrow(Throwable throwable); 设定预期方法调用次数 通过以上的函数,您可以对 Mock 对象特定行为的预期输出进行设定。除了对预期输出进行设定, IExpectationSetters<T>times(int count); 该方法可以 Mock 对象方法的调用次数进行确切的设定。假设我们希望 mockResultSet 的 mockResultSet.getString(1); expectLastCall().andReturn("My return value").times(3);
除了设定确定的调用次数, 某些方法的返回值类型是 void,对于这一类方法,我们无需设定返回值,只要设置调用次数就可以了。以 mockResultSet.close(); expectLastCall().times(3, 5); 为了简化书写,EasyMock 还提供了另一种设定 Mock 对象行为的语句模式。对于上例,您还可以将它写成: expect(mockResult.close()).times(3, 5);
将 Mock 对象切换到 Replay 状态在生成 Mock 对象和设定 Mock 对象行为两个阶段,Mock 对象的状态都是 Record 。在这个阶段,Mock 对象会记录用户对预期行为和输出的设定。 在使用 Mock 对象进行实际的测试前,我们需要将 Mock 对象的状态切换为 Replay。在 Replay 状态,Mock 对象能够根据设定对特定的方法调用作出预期的响应。将 Mock 对象切换成 Replay 状态有两种方式,您需要根据 Mock 对象的生成方式进行选择。如果 Mock 对象是通过 replay(mockResultSet); 如果 Mock 对象是通过 control.replay(); 以上的语句能将在第1节中生成的 mockConnection、mockStatement 和 mockResultSet 等3个 Mock 对象都切换成 Replay 状态。 调用 Mock 对象方法进行单元测试为了更好的说明 EasyMock 的功能,我们引入 src.zip 中的示例来解释 Mock 对象在实际测试阶段的作用。其中所有的示例代码都可以在 src.zip 中找到。如果您使用的 IDE 是 Eclipse,在导入 src.zip 之后您可以看到 Workspace 中增加的 project(如下图所示)。 图2:导入 src.zip 后的 Workspace下面是示例代码中的一个接口 清单2:SalesOrder 接口public interface SalesOrder { …… public void loadDataFromDB(ResultSet resultSet) throws SQLException; public String getPriceLevel(); } 其实现类 清单3:SalesOrderImpl 实现public class SalesOrderImpl implements SalesOrder { ...... public void loadDataFromDB(ResultSet resultSet) throws SQLException { orderNumber = resultSet.getString(1); region = resultSet.getString(2); totalPrice = resultSet.getDouble(3); } ...... } 方法 清单4:完整的TestCasepublic class SalesOrderTestCase extends TestCase { public void testSalesOrder() { IMocksControl control = EasyMock.createControl(); ...... ResultSet mockResultSet = control.createMock(ResultSet.class); try { ...... mockResultSet.next(); expectLastCall().andReturn(true).times(3); expectLastCall().andReturn(false).times(1); mockResultSet.getString(1); expectLastCall().andReturn("DEMO_ORDER_001").times(1); expectLastCall().andReturn("DEMO_ORDER_002").times(1); expectLastCall().andReturn("DEMO_ORDER_003").times(1); mockResultSet.getString(2); expectLastCall().andReturn("Asia Pacific").times(1); expectLastCall().andReturn("Europe").times(1); expectLastCall().andReturn("America").times(1); mockResultSet.getDouble(3); expectLastCall().andReturn(350.0).times(1); expectLastCall().andReturn(1350.0).times(1); expectLastCall().andReturn(5350.0).times(1); control.replay(); ...... int i = 0; String[] priceLevels = { "Level_A", "Level_C", "Level_E" }; while (mockResultSet.next()) { SalesOrder order = new SalesOrderImpl(); order.loadDataFromDB(mockResultSet); assertEquals(order.getPriceLevel(), priceLevels[i]); i++; } control.verify(); } catch (Exception e) { e.printStackTrace(); } } } 在这个示例中,我们首先创建了 对 Mock 对象的行为进行验证在利用 Mock 对象进行实际的测试过程之后,我们还有一件事情没有做:对 Mock 对象的方法调用的次数进行验证。 为了验证指定的方法调用真的完成了,我们需要调用 verify(mockResultSet); 如果Mock对象是有 control.verify(); 将对 control 实例所生成的 Mock 对象 mockConnection、mockStatement 和 mockResultSet 等进行验证。如果将上例中 图3:Mock对象验证失败Mock 对象的重用为了避免生成过多的 Mock 对象,EasyMock 允许对原有 Mock 对象进行重用。要对 Mock 对象重新初始化,我们可以采用 reset 方法。和 replay 和 verify 方法类似,EasyMock 提供了两种 reset 方式:(1)如果 Mock 对象是由 在重新初始化之后,Mock 对象的状态将被置为 Record 状态。 3.在 EasyMock 中使用参数匹配器EasyMock 预定义的参数匹配器在使用 Mock 对象进行实际的测试过程中,EasyMock 会根据方法名和参数来匹配一个预期方法的调用。EasyMock 对参数的匹配默认使用 mockStatement.executeQuery("SELECT * FROM sales_order_table"); expectLastCall().andStubReturn(mockResultSet); 在实际的调用中,我们可能会遇到 SQL 语句中某些关键字大小写的问题,例如将 SELECT 写成 Select,这时在实际的测试中,EasyMock 所采用的默认匹配器将认为这两个参数不匹配,从而造成 Mock 对象的预期方法不被调用。EasyMock 提供了灵活的参数匹配方式来解决这个问题。如果您对 mockStatement 具体执行的语句并不关注,并希望所有输入的字符串都能匹配这一方法调用,您可以用 mockStatement.executeQuery( anyObject() ); expectLastCall().andStubReturn(mockResultSet);
自定义参数匹配器预定义的参数匹配器可能无法满足一些复杂的情况,这时你需要定义自己的参数匹配器。在上一节中,我们希望能有一个匹配器对 SQL 中关键字的大小写不敏感,使用 要定义新的参数匹配器,需要实现 清单5:自定义参数匹配器SQLEqualspublic class SQLEquals implements IArgumentMatcher { private String expectedSQL = null; public SQLEquals(String expectedSQL) { this.expectedSQL = expectedSQL; } ...... public boolean matches(Object actualSQL) { if (actualSQL == null && expectedSQL == null) return true; else if (actualSQL instanceof String) return expectedSQL.equalsIgnoreCase((String) actualSQL); else return false; } } 在实现了 清单6:自定义参数匹配器 SQLEquals 静态方法public static String sqlEquals(String in) { reportMatcher(new SQLEquals(in)); return in; } 这样,我们自定义的 sqlEquals 匹配器就可以使用了。我们可以将上例中的 mockStatement.executeQuery(sqlEquals("SELECT * FROM sales_order_table")); expectLastCall().andStubReturn(mockResultSet);
4.特殊的 Mock 对象类型到目前为止,我们所创建的 Mock 对象都属于 EasyMock 默认的 Mock 对象类型,它对预期方法的调用次序不敏感,对非预期的方法调用抛出 AssertionError。除了这种默认的 Mock 类型以外,EasyMock 还提供了一些特殊的 Mock 类型用于支持不同的需求。 Strick Mock 对象如果 Mock 对象是通过 ResultSet strickMockResultSet = createStrickMock(ResultSet.class); 类似于 createMock,我们同样可以用 IMocksControl control = EasyMock.createStrictControl(); ResultSet strickMockResultSet = control.createMock(ResultSet.class); Nice Mock 对象使用 5.EasyMock 的工作原理EasyMock 是如何为一个特定的接口动态创建 Mock 对象,并记录 Mock 对象预期行为的呢?其实,EasyMock 后台处理的主要原理是利用 图4:EasyMock主要功能类和开发人员联系最紧密的是 我们知道 Mock 对象有两种创建方式:一种是通过
当我们调用 在创建动态代理的同时,应当提供 创建 Mock 对象下图是创建 Mock 对象的时序图: 图5:创建 Mock 对象时序图当 记录 Mock 对象预期行为记录 Mock 的预期行为可以分为两个阶段:预期方法的调用和预期输出的设定。在外部程序中获得的 Mock 对象,其实就是由 图6:调用预期方法时序图当 在记录 Mock 对象预期行为时,Mock 对象的状态是 Record 状态,因此 根据 在对预期方法进行调用之后,我们可以对该方法的预期输出进行设定。我们以 expectLastCall().andReturn(X value).times(int times)
图7:设定预期输出时序图在预期方法被调用时,Mock 对象对应的 当 在 Replay 状态下调用 Mock 对象方法
图8:调用 Mock 对象方法时序图在 Replay 状态下, 6.使用 EasyMock 进行单元测试小结如果您需要在单元测试中构建 Mock 对象来模拟协同模块或一些复杂对象,EasyMock 是一个可以选用的优秀框架。EasyMock 提供了简便的方法创建 Mock 对象:通过定义 Mock 对象的预期行为和输出,你可以设定该 Mock 对象在实际测试中被调用方法的返回值、异常抛出和被调用次数。通过创建一个可以替代现有对象的 Mock 对象,EasyMock 使得开发人员在测试时无需编写自定义的 Mock 对象,从而避免了额外的编码工作和因此引入错误的机会。 下载
|
|