发个博测试

来源:互联网 发布:阿里云邮箱在线客服 编辑:程序博客网 时间:2024/06/16 00:37

Mock 方法

单元测试是对应用中的某一个模块的功能进行验证。在单元测试中,我们常遇到的问题是应用中其它的协同模块尚未开发完成,或者被测试模块需要和一些不容易构造、比较复杂的对象进行交互。另外,由于不能肯定其它模块的正确性,我们也无法确定测试中发现的问题是由哪个模块引起的。

Mock 对象能够模拟其它协同模块的行为,被测试模块通过与Mock 对象协作,可以获得一个孤立的测试环境。此外,使用Mock 对象还可以模拟在应用中不容易构造(如HttpServletRequest 必须在Servlet 容器中才能构造出来)和比较复杂的对象(如JDBC 中的ResultSet 对象),从而使测试顺利进行

简介

手动的构造 Mock 对象会给开发人员带来额外的编码量,而且这些为创建Mock 对象而编写的代码很有可能引入错误。目前,有许多开源项目对动态构建Mock 对象提供了支持,这些项目能够根据现有的接口或类动态生成,这样不仅能避免额外的编码工作,同时也降低了引入错误的可能。

EasyMock 是一套用于通过简单的方法对于给定的接口生成Mock 对象的类库。它提供对接口的模拟,能够通过录制、回放、检查三步来完成大体的测试过程,可以验证方法的调用种类、次数、顺序,可以令Mock 对象返回指定的值或抛出指定异常。通过EasyMock,我们可以方便的构造Mock 对象从而使单元测试顺利进行。

个人认知:easyMock可以将自己模拟成任何对象,来提供测试过程中所需要的数据访问。比如访问数据库时,我们可以不去连接数据库,而是由easyMock模拟一个resultSet,从中获取我们想要的数据。。很实用的

1.    EasyMock默认只支持为接口生成 Mock 对象,如果需要为类生成 Mock 对象,在 EasyMock 的主页上有扩展包可以实现此功能。

2.    初步实用easyMock生成mock对象(网上资料)

这个过程大致可以划分为以下几个步骤:

  • 使用 EasyMock 生成 Mock 对象;
  • 设定 Mock 对象的预期行为和输出;
  • Mock 对象切换到 Replay 状态;
  • 调用 Mock 对象方法进行单元测试;
  • Mock 对象的行为进行验证。

EasyMock 生成 Mock 对象

根据指定的接口或类,EasyMock 能够动态的创建 Mock 对象(EasyMock 默认只支持为接口生成 Mock 对象,如果需要为类生成 Mock 对象,在 EasyMock 的主页上有扩展包可以实现此功能),我们以 ResultSet 接口为例说明EasyMock的功能。java.sql.ResultSet 是每一个 Java 开发人员都非常熟悉的接口:


1ResultSet 接口

               

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;

......

}

 

通常,构建一个真实的 RecordSet 对象需要经过一个复杂的过程:在开发过程中,开发人员通常会编写一个 DBUtility 类来获取数据库连接 Connection,并利用 Connection 创建一个 Statement。执行一个 Statement 可以获取到一个或多个 ResultSet 对象。这样的构造过程复杂并且依赖于数据库的正确运行。数据库或是数据库交互模块出现问题,都会影响单元测试的结果。

我们可以使用 EasyMock 动态构建 ResultSet 接口的 Mock 对象来解决这个问题。一些简单的测试用例只需要一个 Mock 对象,这时,我们可以用以下的方法来创建 Mock 对象:

ResultSet mockResultSet = createMock(ResultSet.class);

 

其中 createMock  org.easymock.EasyMock 类所提供的静态方法,你可以通过 static import 将其引入(注:static import java 5.0 所提供的新特性)。

如果需要在相对复杂的测试用例中使用多个 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 类的 createControl 方法能创建一个接口 IMocksControl 的对象,该对象能创建并管理多个 Mock 对象。如果需要在测试中使用多个 Mock 对象,我们推荐您使用这一机制,因为它在多个 Mock 对象的管理上提供了相对便捷的方法。

如果您要模拟的是一个具体类而非接口,那么您需要下载扩展包 EasyMock Class Extension 2.2.2。在对具体类进行模拟时,您只要用 org.easymock.classextension.EasyMock 类中的静态方法代替 org.easymock.EasyMock 类中的静态方法即可。

Mock 对象的预期行为和输出

在一个完整的测试过程中,一个 Mock 对象将会经历两个状态:Record 状态和 Replay 状态。Mock 对象一经创建,它的状态就被置为 Record。在 Record 状态,用户可以设定 Mock 对象的预期行为和输出,这些对象行为被录制下来,保存在 Mock 对象中。

添加 Mock 对象行为的过程通常可以分为以下3步:

  • Mock 对象的特定方法作出调用;
  • 通过 org.easymock.EasyMock 提供的静态方法 expectLastCall 获取上一次方法调用所对应的 IExpectationSetters 实例;
  • 通过 IExpectationSetters 实例设定 Mock 对象的预期输出。

设定预期返回值

Mock 对象的行为可以简单的理解为 Mock 对象方法的调用和方法调用所产生的输出。在 EasyMock 2.3 中,对 Mock 对象行为的添加和设置是通过接口 IExpectationSetters 来实现的。Mock 对象方法的调用可能产生两种类型的输出:(1)产生返回值;(2)抛出异常。接口 IExpectationSetters 提供了多种设定预期输出的方法,其中和设定返回值相对应的是 andReturn 方法:

IExpectationSetters<T> andReturn(T value);

 

我们仍然用 ResultSet 接口的 Mock 对象为例,如果希望方法 mockResult.getString(1) 的返回值为 "My returnvalue",那么你可以使用以下的语句:

mockResultSet.getString(1);

expectLastCall().andReturn("My return value");

 

以上的语句表示 mockResultSet  getString 方法被调用一次,这次调用的返回值是 "My returnvalue"。有时,我们希望某个方法的调用总是返回一个相同的值,为了避免每次调用都为 Mock 对象的行为进行一次设定,我们可以用设置默认返回值的方法:

void andStubReturn(Object value);

 

假设我们创建了 Statement  ResultSet 接口的 Mock 对象 mockStatement mockResultSet,在测试过程中,我们希望 mockStatement 对象的 executeQuery 方法总是返回 mockResultSet,我们可以使用如下的语句

mockStatement.executeQuery("SELECT * FROM sales_order_table");

expectLastCall().andStubReturn(mockResultSet);

 

EasyMock 在对参数值进行匹配时,默认采用 Object.equals() 方法。因此,如果我们以 "select * fromsales_order_table" 作为参数,预期方法将不会被调用。如果您希望上例中的 SQL 语句能不区分大小写,可以用特殊的参数匹配器来解决这个问题,我们将在 " EasyMock 中使用参数匹配器" 一章对此进行说明。

设定预期异常抛出

对象行为的预期输出除了可能是返回值外,还有可能是抛出异常。IExpectationSetters 提供了设定预期抛出异常的方法:

IExpectationSetters<T> andThrow(Throwable throwable);

 

和设定默认返回值类似,IExpectationSetters 接口也提供了设定抛出默认异常的函数:

void andStubThrow(Throwable throwable);

 

设定预期方法调用次数

通过以上的函数,您可以对 Mock 对象特定行为的预期输出进行设定。除了对预期输出进行设定,IExpectationSetters 接口还允许用户对方法的调用次数作出限制。在 IExpectationSetters 所提供的这一类方法中,常用的一种是 times 方法:

IExpectationSetters<T>times(int count);

 

该方法可以 Mock 对象方法的调用次数进行确切的设定。假设我们希望 mockResultSet  getString 方法在测试过程中被调用3次,期间的返回值都是 "My return value",我们可以用如下语句:

mockResultSet.getString(1);

expectLastCall().andReturn("My return value").times(3);

 


注意到 andReturn  andThrow 方法的返回值依然是一个 IExpectationSetters 实例,因此我们可以在此基础上继续调用 times 方法。

除了设定确定的调用次数,IExpectationSetters 还提供了另外几种设定非准确调用次数的方法:
times(int minTimes, int maxTimes):该方法最少被调用 minTimes 次,最多被调用 maxTimes 次。
atLeastOnce():该方法至少被调用一次。
anyTimes():该方法可以被调用任意次。

某些方法的返回值类型是 void,对于这一类方法,我们无需设定返回值,只要设置调用次数就可以了。以 ResultSet 接口的 close 方法为例,假设在测试过程中,该方法被调用35次:

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 对象是通过 org.easymock.EasyMock 类提供的静态方法 createMock 生成的(第1节中介绍的第一种 Mock 对象生成方法),那么EasyMock 类提供了相应的 replay 方法用于将 Mock 对象切换为 Replay 状态:

replay(mockResultSet);

 

如果 Mock 对象是通过 IMocksControl 接口提供的 createMock 方法生成的(第1节中介绍的第二种Mock对象生成方法),那么您依旧可以通过 IMocksControl 接口对它所创建的所有 Mock 对象进行切换:

control.replay();

 

以上的语句能将在第1节中生成的 mockConnectionmockStatement mockResultSet 3 Mock 对象都切换成 Replay 状态。

3.    我自己的代码和理解:

/**

     * by EasyMock

     * 一般情况下分为2个步骤:Recordreplay就够了,verifyreset用的不是很多

Record 部分就是首先调用方法,然后设置这次调用想要获得的结果。andStubReturn会使结果恒为一个值

    andReturn 是在本次或有限的几次返回某个值

replay 之后就可以调用已经记录过的方法了。

Verify  是当设置了调用次数,假如最后真实调用次数和设定的不符合就会抛异常。

Reset  重新设置到Record状态。

     * @throws Exception

     */

    @Test

    public void easyMockTest() throws Exception {

       ResultSet mockResultSet =EasyMock.createMock(ResultSet.class);

 

       // Record

       mockResultSet.getString(1);

       EasyMock.expectLastCall().andReturn("My return Value").times(3);

       // replay

       EasyMock.replay(mockResultSet);

       System.out.println(mockResultSet.getString(1));

       System.out.println(mockResultSet.getString(1));

       // verify 如果调用次数不对会出错

       EasyMock.verify(mockResultSet);

       // 重用的话

       // EasyMock.reset(mockResultSet);

    }

 

    /**

     * by IMocksControl

       这种情况适合有多个mock对象的情况,不过在一个mock对象的时候用也会比较方便,自我推荐这种方式。

       可以统一修改所有mock的状态

     */

    @Test

    public void easyControlMockTest() {

       // get control

       IMocksControl control =EasyMock.createControl();

       ResultSet mockRs =control.createMock(ResultSet.class);

       try {

           // record

           mockRs.getString(2);

           EasyMock.expectLastCall().andReturn("one").times(1);

           EasyMock.expectLastCall().andReturn("two").times(1);

           // replay

           control.replay();

 

           System.out.println(mockRs.getString(2));

           System.out.println(mockRs.getString(2));

           // verify

           control.verify();

           // reset

           // control.reset();

       } catch (SQLException e) {

           e.printStackTrace();

       }

    }

 

    /**对象的重用

     * 在使用 Mock 对象进行实际的测试过程中,EasyMock 会根据方法名和参数来匹配一个预期方法的调用。

     * EasyMock 对参数的匹配默认使用equals() 方法进行比较。这可能会引起一些问题。

     * <p>

     * 例如在上一章节中创建的mockStatement对象 在实际的调用中,我们可能会遇到 SQL

     * 语句中某些关键字大小写的问题,例如将 SELECT 写成 Select,这时在实际的测试中,EasyMock

     * 所采用的默认匹配器将认为这两个参数不匹配,从而造成 Mock 对象的预期方法不被调用。EasyMock

     * 提供了灵活的参数匹配方式来解决这个问题。如果您对 mockStatement

     * 具体执行的语句并不关注,并希望所有输入的字符串都能匹配这一方法调用

     * 可以用 org.easymock.EasyMock 类所提供的anyObject 方法来代替参数中的 SQL 语句

     */

    @Test

    public void anyParamTest() {

       IMocksControl control =EasyMock.createControl();

      

       Statement mockStatement =control.createMock(Statement.class);

       ResultSet mockRs = control.createMock(ResultSet.class);

       try {

           mockStatement.executeQuery((String)EasyMock.anyObject());

           EasyMock.expectLastCall().andStubReturn(mockRs);

          

           control.replay();

           ResultSetrs = mockStatement.executeQuery("select * from table");

       } catch (SQLException e) {

           e.printStackTrace();

       }

}

预定义的参数匹配器可能无法满足一些复杂的情况,这时你需要定义自己的参数匹配器。在上一节中,我们希望能有一个匹配器对 SQL 中关键字的大小写不敏感,使用 anyObject 其实并不是一个好的选择。对此,我们可以定义自己的参数匹配器 SQLEquals

要定义新的参数匹配器,需要实现 org.easymock.IArgumentMatcher 接口。其中:

matches(Object actual) 方法应当实现输入值和预期值的匹配逻辑

appendTo(StringBuffer buffer) 方法中,你可以添加当匹配失败时需要显示的信息

以下是 SQLEquals 实现的部分代码:


清单5:自定义参数匹配器SQLEquals

 //matches方法做的主要是:利用equalsIgnoreCase来比较内容,若相同,返回真!

public 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;

  }

}

 

在实现了 IArgumentMatcher 接口之后,我们需要写一个静态方法将它包装一下。这个静态方法的实现需要将 SQLEquals 的一个对象通过 reportMatcher 方法报告给EasyMock


清单6:自定义参数匹配器 SQLEquals 静态方法

               

public static String sqlEquals(String in) {

  reportMatcher(new SQLEquals(in));

  return in;

}

 

这样,我们自定义的 sqlEquals 匹配器就可以使用了。我们可以将上例中的 executeQuery 方法设定修改如下:

mockStatement.executeQuery(sqlEquals("SELECT * FROM sales_order_table"));

expectLastCall().andStubReturn(mockResultSet);

 


在使用 executeQuery("select* from sales_order_table") 进行方法调用时,该预期行为将被匹配。

的工作原理

呵呵,果然是动态代理,猜到了。

EasyMock 后台处理时,通过Proxy为指定的接口创建一个动态代理(所以若是代理类的话,用Proxy是实现不了的,还需要cglibasm等等包的支持才行。这个动态代理就是我们编码中用到的mock对象。easyMock还为这个动态代理提供了一个InvocationHandler接口的实现,这个实现类的主要功能就是将动态代理的预期行为记录在某个映射表中,实际调用时从映射表中取出相应预期输出。

6.   和开发人员联系最紧密的是 EasyMock 类,这个类提供了 createMockreplayverify 等方法以及所有预定义的参数匹配器。

我们知道 Mock 对象有两种创建方式:一种是通过 EasyMock 类提供的 createMock 方法创建,另一种是通过 EasyMock 类的createControl 方法得到一个 IMocksControl 实例,再由这个 IMocksControl 实例创建 Mock 对象。其实,无论通过哪种方法获得Mock 对象,EasyMock都会生成一个 IMocksControl 的实例,只不过第一种方式中的 IMocksControl 的实例对开发人员不可见而已。这个 IMocksControl 的实例,其实就是 MocksControl 类的一个对象。MocksControl 类提供了 andReturnandThrowtimescreateMock 等方法。

 

MocksControl 类中包含了两个重要的成员变量,分别是接口 IMocksBehavior  IMocksControlState 的实例。其中,IMocksBehavior的实现类 MocksBehavior EasyMock 的核心类,它保存着一个 ExpectedInvocationAndResult 对象的一个列表,而ExpectedInvocationAndResult 对象中包含着 Mock 对象方法调用和预期结果的映射。MocksBehavior 类提供了 addExpected addActual 方法用于添加预期行为和实际调用。

MocksControl 类中包含的另一个成员变量是 IMocksControlState 实例。IMocksControlState 拥有两个不同的实现类:RecordState ReplayState。顾名思义,RecordState  Mock 对象在 Record状态时的支持类,它提供了 invoke 方法在 Record状态下的实现。此外,它还提供了 andReturnandThrowtimes 等方法的实现。ReplayState  Mock 对象在 Replay状态下的支持类,它提供了 invoke 方法在 Replay状态下的实现。在ReplayState 中,andReturnandThrowtimes 等方法的实现都是抛出IllegalStateException,因为在 Replay阶段,开发人员不应该再调用这些方法。

当我们调用 MocksControl  createMock 方法时,该方法首先会生成一个 JavaProxyFactory 类的对象。JavaProxyFactory 是接口IProxyFactory 的实现类,它的主要功能就是通过 java.lang.reflect.Proxy 对指定的接口创建动态代理实例,也就是开发人员在外部看到的Mock 对象。

在创建动态代理的同时,应当提供 InvocationHandler 的实现类。MockInvocationHandler 实现了这个接口,它的 invoke 方法主要的功能是根据 Mock 对象状态的不同而分别调用 RecordState  invoke 实现或是 ReplayState  invoke 实现。