开发环境版本

JDK 1.5
JUnit 3.8
EasyMock 2.2
Eclipse 3.1.2
MyEclipse 4.1.0
注:EasyMock2.0+只支持JDK1.5+,因为其代码中用到了很多新增特性,比如for(:)、method(...)等用法。EasyMock最初的1.0版本似乎支持对实体类的模拟,但是貌似在很面的版本中和快就取消了这个功能。

EasyMock介绍

EasyMock provides Mock Objects for interfaces in JUnit tests by generating them on the fly using Java's proxy mechanism. Due to EasyMock's unique style of recording expectations, most refactorings will not affect the Mock Objects. So EasyMock is a perfect fit for Test-Driven Development.
EasyMock是用于JUnit中的虚拟测试辅助包,它提供对interface类的模拟,能够通过录制、回放、检查三步来完成大体的测试过程,可以验证方法的调用种类、次数、顺序,可以令mock对象返回指定的值或抛出指定异常。

下载及安装

EasyMock是放在sourceforge上的一个开源项目,可以自由下载,从那里下载到对应版本的压缩包后可以直接解压,对于开发,我们只需要easymock.jar这个文件,把它添加到Eclipse的项目jar库里就可以使用了。
另外,由于它是用于在JUnit环境下测试的包,所以在实际使用的时候还需要添加JUnit.jar

概念准备

在正式编程之前,还需要搞懂一些相关概念,这些有的也是类的名字,但是在这里主要是理解他所代表的概念,方便进一步使用EasyMock。
Method、Arguments、Invocation
ExpectedInvocation、actual
MockControl、Mock
reset()、replay()、verify()等。

method、arguments,invocation

这三个概念有从属关系,method代表要模拟的类的一个方法,arguments是这个方法的入口参数,invocation代表一次模拟类的某 个方法的调用,它包含一个method,若干argument(但是在这里不包括返回值)。在EasyMock中有Invocation这个类,含有 Object[] arguments、Method method、Object mock参数。

ExpectedInvocation,actual

前者是代表一次预期的方法调用,这里的预期是指加入了Matcher(s)的Invocation,不仅要具有Invocation的特征,还要加 上对其入口参数的检验器(Matcher),这一概念的引入是为了保证可以判断类似数组这种对象的比较关系,或者为入口参数设定合法条件(不仅是简单的相 等,还有大于等于,字符串的endwith等,用户只要按照它的规则,也可以自己制作专门的matcher)。与其相关的类有 ExpectedInvocation、ExpectedInvocationAndResult、 ExpectedInvocationAndResults,后面两个类加入了指定的返回值,是对有返回值的函数适用的。
而后面的actual是经常出现在EasyMock源代码中作为参数使用的单词,用于代表replay过程中的一次实际的方法调用,和Invocation属于一种概念。

MockControl、Mock

MockControl是控制类,他负责建立整个框架所需的资源,其成员behavior和RecordState state用于保存方法调用的序列,一个control可以同时管理多个mock。Mock对象对应一个你需要测试的待测试类,它会自动建立 JavaProxyFactory<toMock>,再由JavaProxyFactory建立Proxy;同时建立的还有 ObjectMethodFilter(他持有一个MockInvocationHandler对象,对hashCode()、equals()、 toString()三个方法进行判断)和MockInvocationHandler(他持有一个control对象,似乎是用于添加 Invocation序列的)。

reset()、replay()、verify()

reset()方法是将control对象复位,其内部现实是靠新建behavior和state两个对象完成的。replay()是结束录制过 程,他会调用RecordState.closeMethod()方法来完成大部分工作。verify()是用于在录制和回放两个步骤完成之后进行预期和 实际结果的检查。

测试结构模型

接口类:BaseInterface

public interface BaseInterface{
public void methodABC();
public SomeValue methodABC(SomeArgument someArgument);
}

实体类:ImplementsBaseInterface、TargetClass

ImplementsBaseInterface继承了BaseInterface接口,提供方法的具体实现。
public class TargetClass{
private BaseInterface member;
……//其他成员
public void somMethods(){
……
member.methodABC();
member.methodABC(someArguments,……);
}
public void setMemeber(BaseInterface member){
this.memeber = member;
}
……//其他方法
}

类及方法之间的关系

而TargetClass可能会有一些内部成员,比如现在我们正好有一个BaseInterface类型的内部成员member,而 TargetClass正好有someMethods()需要调用到BaseInterface中的methodABC()和methodABC (SomeArgument someArgument)。

测试类:TargetClassTest

TargetClassTest是一个JUnit Test Case类,用于测试TargetClass这个类,这是我们的目的。
import static org.easymock.EasyMock.*;
import ……
public class TargetClassTest{
private TargetClass target;
private BaseInterface mock;
private IMocksControl ctrl;
public void setUp(){
ctrl = createStrictControl();
mock = ctrl.createMock( ImplementsBaseInterface.class ); //建立easymock测试类
target = new TargetClass();
target.setMember(mock); //将刚刚建立的mock加入到target的成员中
}
public void testSomeMethods() throws Exception{
……//准备工作,生成一些将要使用的对象
ctrl.reset(); //初始化
mock.methodABC();
expect(mock.methodABC(someArguments,……)).andReturn(SomeValue); //由于这个方法有返回值,所以需要用andReturn()方法来指定,否则测试的时候会扔出异常
ctrl.replay(); //结束整个control的recorder过程,下面进入回放
target.someMethods();
ctrl.verify(); //结束回放,进行检查,主要检查回放过程和录制过程是否一致,对于mock对象的方法调用种类、数量、顺序是否符合要求进行验证。如果有错误出现就会抛出异常,但是没有自动检查返回值的功能。
}
}

对于模型的说明

control

当只有一个mock对象的时候,我们也可以不手动生成control对象,而是直接调用EasyMock类中的createMock(Class clazz)方法,这样的话,相应的reset()、replay()、verify()方法也不能使用control的,而要使用EasyMock类中 的reset(mock)、replay(mock)、verify(mock)方法,只对单一的一个mock对象进行操作,在有多个mock对象的时候 可以做到异步操作(但是这样做似乎意义不大)。但是实际上,control对象是一定会生成的,无论你是否手动生成。
生成control对象的好处在于当有多个mock的时候可以一起执行reset()、replay()、verify()操作。

andReturn()

这个方法只是用于当mock对象的方法需要有返回值的情况下,手动设置这个方法的返回值给调用的测试类的。在本例中methodABC (arguments,……)方法就需要在recorder的时候用andReturn()方法指定返回值给TargetClass。如果对于有返回值的 方法不指定其返回值,在测试的时候会抛出"java.lang.IllegalStateException: missing behavior definition for the preceeding method call XXX"异常。

mock的种类

EasyMock提供了三种Mock类型:StrictMock、NiceMock、Mock。
种类 生成函数 检查顺序 检查方法是否调用 对未说明的方法调用
Mock createMock() 抛出异常
NiceMock createNiceMock() 返回0、null,不抛出异常
StrictMock createStrictMock() 抛出异常

一点补充

对于recorder期的方法调用还有一些设定调用次数的方法比较重要,可能会用到,比如说times(),可以设定调用出现的次数使用方法类似andReturn(value)
expect(***).times(3); //指定这个方法调用出现三次
相当于
expect(***);
expect(***);
expect(***);
还可以设定它出现的次数为不封顶,或在一个范围之内,这些方法都可以在MocksControl类中找到
times(int);
times(min , max); //将max设为整数最大值就是不封顶,其实设大点也就够用了
anyTimes();
once();

杂谈EasyMock内部原理

看了一天半的时间,对他的原理有了一些认识,但是还不全面,对一些深层的原理还了解得不太清楚,先简单谈一谈目前的认识,很可能会有很多错误的地方,如果以后发现,会陆续修改。

EasyMock是利用线程来捕获mock对象函数调用的,所以他特别有一个LastControl类,里面有三个成员, 用于将线程与三个不同的类相关联,matcher是匹配器、argument是入口参数、control中含有大部分主要内容。
 private static final WeakHashMap<Thread, MocksControl> threadToControl 
= new WeakHashMap<Thread, MocksControl>();
 private static final WeakHashMap<Thread, Stack<Object[]>> threadToCurrentArguments
= new WeakHashMap<Thread, Stack<Object[]>>();
 private static final WeakHashMap<Thread, Stack<IArgumentMatcher>> threadToArgumentMatcherStack
= new WeakHashMap<Thread, Stack<IArgumentMatcher>>();
困惑:matcher和argument应该都是和指定的Invocation相关联,但是这里只存了control,而一个control可以有多个mock,一个mock又能有多个method过程,怎样确定matcher、argument和method的对应关系?是通过线程实现的么?

他将recorder到的method及其参数、times都记录到线性表中,而replay的对应method、参数、times也都记录下来,进行对比,从而实现verify()功能。

Invocation对应一次具体的方法调用过程,可能包含返回值Result。

与之对应的ExpectedInvocation系列类加入了matcher和result,也就是加入了预期的功能。

UnorderedBehavior是最底层的Behavior类,其中主要有addExpected()和addActual ()两个方法,应该可以完成预期的保存和检验两个功能。他有一个ExpectedInvocationAndResults的List,可以记录一些列的 方法预期,可以指定应记录一个mock,但是这点还没有证实。

recorder和replay都是主要靠IMocksControlState接口的实现类:RecorderState和ReplayState,各司其职。

RecorderState的成员是:ExpectedInvocation lastInvocation、Result lastResult、IMocksBehavior behavior behavior用于记录所有提交进来的ExpectedInvocation,而那两个lastXXX,应该是用于保留最后一条记录。verify()方法会直接抛出异常:calling verify is not allowed in record state

ReplayState只有一个IMocksBehavior behavior成员,方法也只有invoke()和verify()不会直接抛出异常。应该是像其名称一样专门用于处理replay时期的验证的