单元测试中的打桩技术

  • Post author:
  • Post category:其他


一、桩是什么

桩,或称桩代码,是指用来代替关联代码或者未实现代码的代码。如果用函数B1来代替B,那么,B称为原函数,B1称为桩函数。打桩就是编写或生成桩代码。

二、打桩的用途

打桩的目的主要有:隔离、补齐、控制。

①隔离是指将测试任务从产品项目中分离出来,使之能够独立编译、链接,并独立运行。隔离的基本方法就是打桩,将测试任务之外的,并且与测试任务相关的代码,用桩来代替,从而实现分离测试任务。例如函数A调用了函数B,函数B又调用了函数C和D,如果函数B用桩来代替,函数A就可以完全割断与函数C和D的关系。

②补齐是指用桩来代替未实现的代码,例如,函数A调用了函数B,而函数B由其他程序员编写,且未实现,那么,可以用桩来代替函数B,使函数A能够运行并测试。补齐在并行开发中很常用。

③控制是指在测试时,人为设定相关代码的行为,使之符合测试需求。

三、常用打桩技术


1、工厂模式

接口是实现多重继承的途



,而生成遵循某个接口的对象的典型方式就是工厂方法设计模式。

对于创建类,几乎在任何时刻,都可以替代为创建一个接口和一个工厂。

这与直接调用构造器不同,我们在工厂对象上调用的是创建方法,而该工厂对象将生成接口的某个实现的对象。理论上,通过这种方式,我们的代码将完全与接口的实现分离,这就使得我们可以透明地将某个实现替换为另一个实现。

interface Service
{
    void method1();

    void method2();
}

interface ServiceFactory
{
    Service getService();
}

class Implementation1 implements Service
{
    public Implementation1()
    {
    }

    @Override
    public void method1()
    {
        System.out.println("Implementation1 method1");
    }

    @Override
    public void method2()
    {
        System.out.println("Implementation1 method2");
    }

}

class Implementation1Factory implements ServiceFactory
{
    @Override
    public Service getService()
    {
        return new Implementation1();
    }
}

class Implementation2 implements Service
{
    public Implementation2()
    {
    }

    @Override
    public void method1()
    {
        System.out.println("Implementation2 method1");
    }

    @Override
    public void method2()
    {
        System.out.println("Implementation2 method2");
    }

}

class Implementation2Factory implements ServiceFactory
{
    @Override
    public Service getService()
    {
        return new Implementation2();
    }
}

public class Factories
{
    public static void serviceConsumer(ServiceFactory fact)
    {
        Service service = fact.getService();
        service.method1();
        service.method2();
    }

    public static void main(String[] args)
    {
        serviceConsumer(new Implementation1Factory());
        serviceConsumer(new Implementation2Factory());
    }
}
output:
Implementation1 method1
Implementation1 method2
Implementation2 method1
Implementation2 method2

如果不是用工厂方法,你的代码就必须在某处指定将要创建的Service的确切类型,以便调用合适的构造器。


2、Mock

mock测试就是在测试过程中,对于某些不容易构造或者 不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。


①Mock 对象与 EasyMock 简介


⑴单元测试与 Mock 方法

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

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


⑵EasyMock 简介

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

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

帮助文档:

http://easymock.org/api/


⑶导入EasyMock包



下载地址:


http://easymock.org/


在eclipse中导入easymock-x.jar



②使用 EasyMock 进行单元测试


⑴EasyMock使用步骤



通过 EasyMock,我们可以为指定的接口动态的创建 Mock 对象,并利用 Mock 对象来模拟协同模块或是领域对象,从而使单元测试顺利进行。这个过程大致可以划分为以下几个步骤:



❶使用 EasyMock 生成 Mock 对象;

❷设定 Mock 对象的预期行为和输出;

❸将 Mock 对象切换到 Replay 状态;

❹调用 Mock 对象方法进行单元测试;

❺对 Mock 对象的行为进行验证。





⑵使用 EasyMock 生成 Mock 对象




根据指定的接口或类,EasyMock 能够动态的创建 Mock 对象(EasyMock 默认只支持为接口生成 Mock 对象,如果需要为类生成 Mock 对象,需要扩展包实现此功能),本文以JDBC 中的 ResultSet 接口为例。



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



一些简单的测试用例只需要一个 Mock 对象,这时,我们可以用以下的方法来创建 Mock 对象:




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





⑶设定 Mock 对象的预期行为和输出




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



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

❶对 Mock 对象的特定方法作出调用;

❷通过org.easymock.EasyMock提供的静态方法expectLastCall/expect获取上一次方法调用所对应的 IExpectationSetters 实例;

❸通过 IExpectationSetters 实例设定 Mock 对象的预期输出。





Ⅰ设定预期返回值




Mock 对象的行为可以简单的理解为 Mock 对象方法的调用和方法调用所产生的输出。对 Mock 对象行为的添加和设置是通过接口 IExpectationSetters 来实现的。Mock 对象方法的调用可能产生两种类型的输出:产生返回值、抛出异常。




设定返回值:IExpectationSetters<T> andReturn(T value);



mockResultSet.getString(1);

expectLastCall().andReturn(“My return value”);

或者expect(mockResultSet.getString(1)).andReturn(“My return value”);




有时,我们希望某个方法的调用总是返回一个相同的值,为了避免每次调用都为 Mock 对象的行为进行一次设定,我们可以用设置默认返回值的方法:void andStubReturn(Object value);



某些方法的返回值类型是 void,对于这一类方法,我们无需设定返回值,只要设置调用次数就可以了。




Ⅱ设定预期异常抛出





IExpectationSetters<T> andThrow(Throwable throwable);



void andStubThrow(Throwable throwable);




Ⅲ设定预期方法调用次数





IExpectationSetters<T>times(int count);



times(int minTimes, int maxTimes):该方法最少被调用 minTimes 次,最多被调用 maxTimes 次。




atLeastOnce():该方法至少被调用一次。



anyTimes():该方法可以被调用任意次。




⑷将 Mock 对象切换到 Replay 状态





在使用 Mock 对象进行实际的测试前,我们需要将 Mock 对象的状态切换为 Replay。在 Replay 状态,Mock 对象能够根据设定对特定的方法调用作出预期的响应。将 Mock 对象切换成 Replay 状态有两种方式,您需要根据 Mock 对象的生成方式进行选择。



如果 Mock 对象是通过 org.easymock.EasyMock 类提供的静态方法 createMock 生成的,那么 EasyMock 类提供了相应的 replay 方法用于将 Mock 对象切换为 Replay 状态:replay(mockResultSet);




如果 Mock 对象是通过 IMocksControl 接口提供的 createMock 方法生成的,那么您依旧可以通过 IMocksControl 接口对它所创建的所有 Mock 对象进行切换: control.replay();




⑸调用 Mock 对象方法进行单元测试





利用 Mock 对象进行实际的测试过程。




⑹对 Mock 对象的行为进行验证





为了验证指定的方法调用真的完成了,我们需要调用 verify 方法进行验证。和 replay 方法类似,您需要根据 Mock 对象的生成方式来选用不同的验证方式。



verify(mockResultSet);

control.verify();




⑺Mock 对象的重用





为了避免生成过多的 Mock 对象,EasyMock 允许对原有 Mock 对象进行重用。要对 Mock 对象重新初始化,我们可以采用 reset 方法。和 replay 和 verify 方法类似,EasyMock 提供了两种 reset 方式:



reset(mockResultSet);

control.reset();



③在 EasyMock 中使用参数匹配器




⑴EasyMock 预定义的参数匹配器





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



如果参数是一个类,不重写参数对应的类的equals()方法,会导致比较判断失效。因为equals()表示是同一类。



EasyMock提供了多个预先定义的参数匹配器,其中比较常用的一些有:

anyObject()表示任意输入值都与预期值相匹配

aryEq(X value):通过Arrays.equals()进行匹配,适用于数组对象;

isNull():当输入值为Null时匹配;

notNull():当输入值不为Null时匹配;

same(X value):当输入值和预期值是同一个对象时匹配;

lt(X value), leq(X value), geq(X value), gt(X value):当输入值小于、小等于、大等于、大于预期值时匹配,适用于数值类型;

startsWith(String prefix), contains(String substring), endsWith(String suffix):当输入值以预期值开头、包含预期值、以预期值结尾时匹配,适用于String类型;

matches(String regex):当输入值与正则表达式匹配时匹配,适用于String类型。




⑵自定义参数匹配器





预定义的参数匹配器可能无法满足一些复杂的情况,这时你需要定义自己的参数匹配器。要定义新的参数匹配器,需要实现 org.easymock.IArgumentMatcher 接口。其中,matches(Object actual) 方法应当实现输入值和预期值的匹配逻辑,而在 appendTo(StringBuffer buffer) 方法中,你可以添加当匹配失败时需要显示的信息。



在实现了 IArgumentMatcher 接口之后,我们需要写一个静态方法将它包装一下。这个静态方法的实现需要将自定义的参数适配器的一个对象通过reportMatcher方法报告给EasyMock:reportMatcher(new Object());



④特殊的Mock对象类型



到目前为止,我们所创建的 Mock 对象都属于 EasyMock 默认的 Mock 对象类型,它对预期方法的调用次序不敏感,对非预期的方法调用抛出 AssertionError。除了这种默认的 Mock 类型以外,EasyMock 还提供了一些特殊的 Mock 类型用于支持不同的需求。





⑴Strick Mock 对象




如果 Mock 对象是通过 EasyMock.createMock() 或是 IMocksControl.createMock() 所创建的,那么在进行 verify 验证时,方法的调用顺序是不进行检查的。如果要创建方法调用的先后次序敏感的 Mock 对象(Strick Mock),应该使用 EasyMock.createStrickMock() 来创建





⑵Nice Mock 对象




使用 createMock() 创建的 Mock 对象对非预期的方法调用默认的行为是抛出 AssertionError,如果需要一个默认返回0,null 或 false 等”无效值”的 “Nice Mock” 对象,可以通过 EasyMock 类提供的 createNiceMock() 方法创建。类似的,你也可以用 IMocksControl 实例来创建一个 Nice Mock 对象。



⑤EasyMock 的工作原理


EasyMock 是如何为一个特定的接口动态创建 Mock 对象,并记录 Mock 对象预期行为的呢?其实,EasyMock 后台处理的主要原理是利用 java.lang.reflect.Proxy 为指定的接口创建一个动态代理,这个动态代理,就是我们在编码中用到的 Mock 对象。EasyMock 还为这个动态代理提供了一个 InvocationHandler 接口的实现,这个实现类的主要功能就是将动态代理的预期行为记录在某个映射表中和在实际调用时从这个映射表中取出预期输出。


3、Spring IOC


在测试代码调用前,先人为修改spring的对象配置文件,把实现类,替换为你的测试桩类。



版权声明:本文为qq_29837161原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。