EasyMock 2是一个提供了简单的方法使用给定接口的模拟对象的库,EasyMock 2在MIT许可证下可用.
模拟对象模拟域代码的部分行为,并能够检查它们作为定义是否被使用.域类可以通过使用模拟对象模拟它们的合作者来被独立的测试.
书写和维护模拟对象常常是一件乏味的任务,而且可能会进入错误.EasyMock 2动态生成模拟对象-不需要书写它们,并且不需要生成代码!
EasyMock 2优点
- 硬写的模拟对象类不再需要
- 支持安全重构的模拟对象:当重命名方法或者重排序方法参数时测试代码不会在运行时打断
- 支持返回值和异常
- 支持检查一个或者多个模拟对象方法调用顺序.
EasyMock 2缺点
- EasyMock 2仅能够工作在Java 5.0或者更高.
EasyMock默认仅支持为接口生成模拟对象.如果想为类生成模拟对象,在EasyMock首页有一个扩展可用.
安装
- Java(至少5.0)是需要的
- 解压EasyMock的zip文件,它包含了一个easymock2.4文件夹.从这个文件夹添加easymock.jar文件到你的classpath
为了执行EasyMock测试,添加tests.zip和junit4.1.jar到你的classpath并运行”java org.easymock.tests.AllTests”.
EasyMock源代码存储在src.zip压缩文件中
用法
软件系统的大多数部分都不可以孤立运行.只有结合其他部分才可以完成一个任务.在多数情况,在单元测试中我们不关心使用的合作者,所以我们视它们为合作者,如果我们关心它,模拟对象会帮助我们孤立测试这个单元.模拟对象替代单元测试中的合作者.
下例使用Collaborator接口
package org.easymock.samples; public interface Collaborator { void documentAdded(String title); void documentChanged(String title); void documentRemoved(String title); byte voteForRemoval(String title); byte[] voteForRemovals(String[] title); }
这个接口的实现是名为ClassUnderTest的类的合作者
public class ClassUnderTest { // ... public void addListener(Collaborator listener) { // ... } public void addDocument(String title, byte[] document) { // ... } public boolean removeDocument(String title) { // ... } public boolean removeDocuments(String[] titles) { // ... }
包含这些类和接口的代码都可以在samples.zip文件中的org.easymock.samples包中找到.
下列例子假设你熟悉JUnit测试框架.虽然这些展示的测试使用JUnit3.8.1,但你也可以使用JUnit4或者TestNG.
第一个模拟对象
我们现在会构建一个测试用例,并使用它来理解EasyMock包的功能.samples.zip包含一个这个测试的一个修改版本.我们的第一个测试将会检查删除一个不存在的文档是否不会导致合作者的一个通知.这是一个没有定义模拟对象的测试.
package org.easymock.samples; import junit.framework.TestCase; public class ExampleTest extends TestCase { private ClassUnderTest classUnderTest; private Collaborator mock; protected void setUp() { classUnderTest = new ClassUnderTest(); classUnderTest.addListener(mock); } public void testRemoveNonExistingDocument() { // This call should not lead to any notification // of the Mock Object: classUnderTest.removeDocument("Does not exist"); } }
对于使用EasyMock 2大多测试来说,我们仅需要一个静态导入org.easymock.EasyMock.这是EasyMock 2中唯一非内部,非过期的类.
import static org.easymock.EasyMock.*; import junit.framework.TestCase; public class ExampleTest extends TestCase { private ClassUnderTest classUnderTest; private Collaborator mock; }
为了得到模拟对象,我们需要:
- 为我们想要模拟的接口创建模拟对象
- 记录期望行为
- 转换模拟对象来重放状态
这是第一个例子:
protected void setUp() { mock = createMock(Collaborator.class); // 1 classUnderTest = new ClassUnderTest(); classUnderTest.addListener(mock); } public void testRemoveNonExistingDocument() { // 2 (we do not expect anything) replay(mock); // 3 classUnderTest.removeDocument("Does not exist"); }
在第三步启动后,mock是Collaborator接口的模拟对象,它期望不调用.这就意味着如果我们改变ClassUnderTest调用这个接口的任何方法,模拟对象会抛出一个AssertionError:
java.lang.AssertionError:
Unexpected method call documentRemoved("Does not exist"):
at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:29)
at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:44)
at $Proxy0.documentRemoved(Unknown Source)
at org.easymock.samples.ClassUnderTest.notifyListenersDocumentRemoved(ClassUnderTest.java:74)
at org.easymock.samples.ClassUnderTest.removeDocument(ClassUnderTest.java:33)
at org.easymock.samples.ExampleTest.testRemoveNonExistingDocument(ExampleTest.java:24)
添加行为
让我们写第二个测试.如果在测试下一个文档被添加到这个类,我们期望在模拟对象上使用文档标题作为参数的一个mock.documentAdded()的调用.
public void testAddDocument() { mock.documentAdded("New Document"); // 2 replay(mock); // 3 classUnderTest.addDocument("New Document", new byte[0]); }
所以在记录状态时(调用replay前),模拟对象不表现为一个模拟对象,但是它记录方法调用.replay方法调用后,它表现为一个模拟对象,检查期望方法是否已经真的完成.
如果classUnderTest.addDocument(”New Document”, new byte[0])使用一个错误的参数调用期望方法,模拟对象会控诉一个AssertionError:
java.lang.AssertionError:
Unexpected method call documentAdded("Wrong title"):
documentAdded("New Document"): expected: 1, actual: 0
at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:29)
at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:44)
at $Proxy0.documentAdded(Unknown Source)
at org.easymock.samples.ClassUnderTest.notifyListenersDocumentAdded(ClassUnderTest.java:61)
at org.easymock.samples.ClassUnderTest.addDocument(ClassUnderTest.java:28)
at org.easymock.samples.ExampleTest.testAddDocument(ExampleTest.java:30)
像所有非期望调用的满意期望一样,所有未击中的期望会显示.(本例没有)如果这个方法被调用太频繁,模拟对象也会控诉:
java.lang.AssertionError:
Unexpected method call documentAdded("New Document"):
documentAdded("New Document"): expected: 1, actual: 1 (+1)
at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:29)
at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:44)
at $Proxy0.documentAdded(Unknown Source)
at org.easymock.samples.ClassUnderTest.notifyListenersDocumentAdded(ClassUnderTest.java:62)
at org.easymock.samples.ClassUnderTest.addDocument(ClassUnderTest.java:29)
at org.easymock.samples.ExampleTest.testAddDocument(ExampleTest.java:30)
验证行为
目前为止有个我们没有掌握的错误.如果我们指定行为,我们想验证它是否真正的被使用.如果在模拟对象上没有方法没调用,当前测试会通过.为了验证指定的行为的行为已被使用,我们必须调用verify(mock);
public void testAddDocument() { mock.documentAdded("New Document"); // 2 replay(mock); // 3 classUnderTest.addDocument("New Document", new byte[0]); verify(mock); }
如果这个方法在模拟对象上没有调用,我们现在会得到下列异常:
java.lang.AssertionError:
Expectation failure on verify:
documentAdded("New Document"): expected: 1, actual: 0
at org.easymock.internal.MocksControl.verify(MocksControl.java:70)
at org.easymock.EasyMock.verify(EasyMock.java:536)
at org.easymock.samples.ExampleTest.testAddDocument(ExampleTest.java:31)
异常详细列表了所有未击中的期望.
期望一个明确的调用次数
到目前为止,我们的测试仅考虑了一个方法的调用.下一个测试会检查附加一个已经存在的文档是否导致mock.documentChanged()使用适当的参数被一次调用.为了确认,我们检查这个例子三次(这是一个例子:-)):
public void testAddAndChangeDocument() { mock.documentAdded("Document"); mock.documentChanged("Document"); mock.documentChanged("Document"); mock.documentChanged("Document"); replay(mock); classUnderTest.addDocument("Document", new byte[0]); classUnderTest.addDocument("Document", new byte[0]); classUnderTest.addDocument("Document", new byte[0]); classUnderTest.addDocument("Document", new byte[0]); verify(mock); }
为了避免mock.documentChanged(”Document”)重复,EasyMock提供了简化操作.我们可以在这个通过expectLastCall()方法返回的对象上使用times(int times)指定调用次数.代码像这样:
public void testAddAndChangeDocument() { mock.documentAdded("Document"); mock.documentChanged("Document"); expectLastCall().times(3); replay(mock); classUnderTest.addDocument("Document", new byte[0]); classUnderTest.addDocument("Document", new byte[0]); classUnderTest.addDocument("Document", new byte[0]); classUnderTest.addDocument("Document", new byte[0]); verify(mock); }
如果这个方法被调用太频繁,我们会得到一个异常来告诉我们这个方法已经被调用太多次.在第一次超过限制调用方法后失败会立刻发生:
java.lang.AssertionError:
Unexpected method call documentChanged("Document"):
documentChanged("Document"): expected: 3, actual: 3 (+1)
at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:29)
at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:44)
at $Proxy0.documentChanged(Unknown Source)
at org.easymock.samples.ClassUnderTest.notifyListenersDocumentChanged(ClassUnderTest.java:67)
at org.easymock.samples.ClassUnderTest.addDocument(ClassUnderTest.java:26)
at org.easymock.samples.ExampleTest.testAddAndChangeDocument(ExampleTest.java:43)
如果有太少调用,verify(mock)抛出一个AssertionError:
java.lang.AssertionError:
Expectation failure on verify:
documentChanged("Document"): expected: 3, actual: 2
at org.easymock.internal.MocksControl.verify(MocksControl.java:70)
at org.easymock.EasyMock.verify(EasyMock.java:536)
at org.easymock.samples.ExampleTest.testAddAndChangeDocument(ExampleTest.java:43)
验证返回类型
为了指定返回值,我们在expect(T value)中包装调用期望,并在通过expect(T value)返回的对象上使用andReturn(Object returnValue)方法指定返回值.
作为一个例子,我们检查文档删除的工作流.如果ClassUnderTest得到文档删除的调用,它请求所有的合作者来为删除调用voteForRemoval(String title)进行它们的投票.正返回值是删除投票.如果所有数的总数是正的,文档会删除并documentRemoved(String title)被在所有合作者上被调用.
public void testVoteForRemoval() { mock.documentAdded("Document"); // expect document addition // expect to be asked to vote for document removal, and vote for it expect(mock.voteForRemoval("Document")).andReturn((byte) 42); mock.documentRemoved("Document"); // expect document removal replay(mock); classUnderTest.addDocument("Document", new byte[0]); assertTrue(classUnderTest.removeDocument("Document")); verify(mock); } public void testVoteAgainstRemoval() { mock.documentAdded("Document"); // expect document addition // expect to be asked to vote for document removal, and vote against it expect(mock.voteForRemoval("Document")).andReturn((byte) -42); replay(mock); classUnderTest.addDocument("Document", new byte[0]); assertFalse(classUnderTest.removeDocument("Document")); verify(mock); }
返回值类型在编译时检查.作为一个例子,下列代码不能编译,因为提供的返回值类型不匹配方法返回值:
expect(mock.voteForRemoval("Document")).andReturn("wrong type");
我们也可以使用通过expectLastCall()返回的对象,替换调用expect(T value)来得到设置返回值的对象.替换:
expect(mock.voteForRemoval("Document")).andReturn((byte) 42);
我们可以使用
mock.voteForRemoval("Document"); expectLastCall().andReturn((byte) 42);
如果这行太长规定这种方式可以使用.因为它不支持在编译时类型检查.
使用异常工作
为了指定异常(更多的是Throwable)被抛出,通过expectLastCall()和expect(T value)返回的对象提供了andThrow(Throwable throwable)这个方法.在调用指定Throwable被抛出的模拟对象后这个方法必须被调用.
非检查时异常(它是RuntimeException,Error和它们的子类)可以从每个方法抛出.检查时异常仅可以从实际抛出它们的方法中抛出.
创建返回值或者异常
有时我们希望我们的模拟对象在实际调用中放回一个值或者抛出一个异常.自从EasyMock2.2后,通过expectLastCall()和expect(T value)返回的对象提供andAnswer(IAnswer answer)这个方法,它允许指定一个IAnswer接口的实现,它可以用来创建返回值或者异常.
在一个IAnswer反馈内部,被传递到模拟调用的参数可以通过EasyMock.getCurrentArguments()得到.如果你使用它们,像重排序这种重构会打破你的测试.你必须警惕.
改变一些方法调用的行为
为一个方法指定一个正在改变的行为也是可能.times,andReturn和andThrow方法可以连接使用.例如,我们定义voteForRemoval(”Document”)方法来
- 起初三次调用返回42.
- 第四次调用抛出一个RuntimeException.
- 一次返回-42
expect(mock.voteForRemoval("Document")) .andReturn((byte) 42).times(3) .andThrow(new RuntimeException(), 4) .andReturn((byte) -42);
放宽调用次数
为了放宽期望调用次数,有附加方法可以被使用代替times(int count):
times(int min, int max)
期望min到max之间次调用
atLeastOnce()
期望至少一次调用
anyTimes()
期望无限次调用
如果没有调用次数被指定,一次调用是被期望的.如果我们希望明确指定这个,可以使用once()或times(1).
严格模拟
在通过EasyMock.createMock()方法返回模拟对象时,方法的调用顺序是不检查的.如果你希望一个检查方法调用顺序严格模拟对象,使用EasyMock.createStrictMock()方法创建它.
如果你不期望的方法被在严格模拟对象上调用,在第一次相冲突的时候期望消息会显示在这点期望调用的方法.verify(mock)方法显示所有未找到的方法调用.
转换顺序检查开关
有时,需要一个模拟对象,它仅检查一些调用的顺序.在记录阶段,你可以通过调用checkOrder(mock, true)开启顺序检查,并通过调用checkOrder(mock, false)转换它关闭.
一个严格的模拟对象和一个普通模拟对象之间有两个不同:
- 一个严格模拟对象创建后顺序检查可用.
- 一个严格模拟对象reset后顺序检查可用.(见重用模拟对象).
使用参数匹配器的灵活期望
为了在模拟对象上使用一个期望匹配一个实际方法调用,对象参数默认使用equals()进行比较. 这可能导致问题.例如,我们考虑下列期望:
String[] documents = new String[] { "Document 1", "Document 2" }; expect(mock.voteForRemovals(documents)).andReturn(42);
如果这个方法调用使用同样内容的另一个数组,我们得到一个异常,因为equals()比较数组的对象标识符.
java.lang.AssertionError:
Unexpected method call voteForRemovals([Ljava.lang.String;@9a029e):
voteForRemovals([Ljava.lang.String;@2db19d): expected: 1, actual: 0
documentRemoved("Document 1"): expected: 1, actual: 0
documentRemoved("Document 2"): expected: 1, actual: 0
at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:29)
at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:44)
at $Proxy0.voteForRemovals(Unknown Source)
at org.easymock.samples.ClassUnderTest.listenersAllowRemovals(ClassUnderTest.java:88)
at org.easymock.samples.ClassUnderTest.removeDocuments(ClassUnderTest.java:48)
at org.easymock.samples.ExampleTest.testVoteForRemovals(ExampleTest.java:83)
对于这次调用指定数组相等是需要的,我们可以使用aryEq方法,它从EasyMock类静态导入:
String[] documents = new String[] { "Document 1", "Document 2" }; expect(mock.voteForRemovals(aryEq(documents))).andReturn(42);
如果你想在一个调用中使用匹配器,你必须为方法调用的所有参数指定匹配器.
有两个预定义参数匹配器可用.
eq(X value)
如果实际参数等于期望值,那么匹配.对于基本类型和对象都可用.
anyBoolean(), anyByte(), anyChar(), anyDouble(), anyFloat(), anyInt(), anyLong(), anyObject(), anyShort()
匹配任何值.对于基本类型和对象都可用.
eq(X value, X delta)
如果实际值在给定的delta范围内等于给定的值那么匹配.对于float和double可用.
aryEq(X value)
如果实际值根据Arrays.equals()方法等于给定的值那么匹配.对于基本类型和对象数组可用.
isNull()
如果实际值为null那么匹配.对于对象可用.
notNull()
如果实际值不为null那么匹配.对于对象可用.
same(X value)
如果实际值和给定值相同那么匹配.对于对象可用.
isA(Class clazz)
如果实际值是给定类的实例,或者它是继承或实现给定类的一个类实例那么匹配.Null值总是返回false.对于对象可用.
lt(X value), leq(X value), geq(X value), gt(X value)
如果实际值小于或小于/等于或大于/等于或大于给定值那么匹配.对于所有数字基本类型或者可比较的类型可用.
startsWith(String prefix), contains(String substring), endsWith(String suffix)
如果实际值以给定值开始/包含给定值/以给定值结尾那么匹配.对于字符串可用.
matches(String regex), find(String regex)
如果实际值/实际值的字串匹配正则表达式那么匹配.对于字符串可用.
and(X first, X second)
如果在first和second中使用的匹配器都匹配,那么匹配.对于所有基本类型和对象可用.
or(X first, X second)
如果在first和second中使用的匹配器中有一个匹配那么匹配.对于所有基本类型和对象可用.
not(X value)
如果在中value使用的匹配器不匹配,那么匹配.
cmpEq(X value)
如果实际值通过Comparable.compareTo(X o)方法等于那么匹配.对于所有基本类型和可比较的类型可用.
cmp(X value, Comparator comparator, LogicalOperator operator)
如果operator为<,<=,>,>= 或==的地方ccomparator.compare(actual, value) operator 0那么匹配.对于对象可用.
capture(Capture capture)
匹配所有值,但是为后续访问捕获它到Capture参数.你可以使用and(someMatcher(…), capture(c))来从这个方法的特定调用来捕获一个参数.
定义你自己的参数匹配器
有时会想要定义自己的参数匹配器.我们会说一个这样的参数匹配器是需要的,它匹配一个异常,如果给定的异常有相同的类型和相等的信息. 它可以这样使用:
IllegalStateException e = new IllegalStateException("Operation not allowed.") expect(mock.logThrowable(eqException(e))).andReturn(true);
完成它两个步骤是必须的:一个新的参数匹配器必须定义,eqException静态方法必须声明.
为了定义新的参数匹配器,我们实现org.easymock.IArgumentMatcher这个接口.这个接口包含两个方法:matches(Object actual)检查实际参数是否匹配给定参数,appendTo(StringBuffer buffer) 附加一个参数匹配器字符串表现到给定的StringBuffer.这个实现是简单的:
import org.easymock.IArgumentMatcher; public class ThrowableEquals implements IArgumentMatcher { private Throwable expected; public ThrowableEquals(Throwable expected) { this.expected = expected; } public boolean matches(Object actual) { if (!(actual instanceof Throwable)) { return false; } String actualMessage = ((Throwable) actual).getMessage(); return expected.getClass().equals(actual.getClass()) && expected.getMessage().equals(actualMessage); } public void appendTo(StringBuffer buffer) { buffer.append("eqException("); buffer.append(expected.getClass().getName()); buffer.append(" with message \""); buffer.append(expected.getMessage()); buffer.append("\"")"); } }
eqException方法必须创建一个使用给定Throwable的参数匹配器,通过reportMatcher(IArgumentMatcher matcher)方法报告它给EasyMock,并返回一个值,所以它可以在调用内部被使用(典型的0,null或者false). 第一次尝试可能看起来像:
public static Throwable eqException(Throwable in) { EasyMock.reportMatcher(new ThrowableEquals(in)); return null; }
然而,如果在这个例子中的logThrowable方法的使用接受Throwable,并且不需要像RuntimeException那样更加明确的东西,这才能工作. 在后面的例子中,我们的代码例子不能编译:
IllegalStateException e = new IllegalStateException("Operation not allowed.") expect(mock.logThrowable(eqException(e))).andReturn(true);
Java 5.0 来营救: 替代使用一个Throwable作为参数和返回值类型定义eqException,我们使用一个继承Throwable的泛型:
public static T eqException(T in) { reportMatcher(new ThrowableEquals(in)); return null; }
重用模拟对象
模拟对象可以通过reset(mock)重设.
如果需要,一个模拟对象也可以通过resetToNice(mock),resetToDefault(mock)或resetToStrict(mock)从一个类型转化为另一个类型.
使用方法桩行为
有时,我们希望我们的模拟对象响应某些方法调用,但是我们希望当它们调用时不检查它们调用的频率,或者设置他们不被调用.这个桩行为可以通过使用andStubReturn(Object value), andStubThrow(Throwable throwable), andStubAnswer(IAnswer
expect(mock.voteForRemoval("Document")).andReturn(42); expect(mock.voteForRemoval(not(eq("Document")))).andStubReturn(-1);
Nice模拟
在通过createMock()返回模拟对象时所有方法的模拟行为是为所有非期望方法调用抛出AssertionError.如果你想要一个”nice”模拟对象 它默认允许所有方法调用并返回一个适当的空值(0,null,或false),使用createNiceMock()代替.
Object方法
equals(), hashCode() 和 toString()三个对象方法行为不能使用EasyMock改变,即使它们是接口的一部分为模拟对象创建使用.
检查模拟之间的方法调用顺序
到目前为止,我们已经看到模拟对象作为一个单一的对象都是通过EasyMock类的静态方法配置.但是许多那些静态方法仅识别了隐藏的模拟对象控制器,并代理给它.一个模拟对象控制器是一个实现IMocksControl接口的对象.
所以替换
IMyInterface mock = createStrictMock(IMyInterface.class); replay(mock); verify(mock); reset(mock);
我们可以使用等同的代码:
IMocksControl ctrl = createStrictControl(); IMyInterface mock = ctrl.createMock(IMyInterface.class); ctrl.replay(); ctrl.verify(); ctrl.reset();
IMocksControl允许创建多个模拟对象,所以检查模拟之间的方法调用顺序是可能的.作为一个例子,我们建立为IMyInterface两个模拟对象,并且我们期望顺序调用mock1.a() 和 mock2.a(), 然后按照次序调用mock1.a() 和 mock2.a()数次,并最后调用mock2.a() 和 mock1.a():
IMocksControl ctrl = createStrictControl(); IMyInterface mock1 = ctrl.createMock(IMyInterface.class); IMyInterface mock2 = ctrl.createMock(IMyInterface.class); mock1.a(); mock2.a(); ctrl.checkOrder(false); mock1.c(); expectLastCall().anyTimes(); mock2.c(); expectLastCall().anyTimes(); ctrl.checkOrder(true); mock2.b(); mock1.b(); ctrl.replay();
命名模拟对象
模拟对象在使用createMock(String name, Class toMock), createStrictMock(String name, Class toMock) 或 createNiceMock(String name, Class toMock)创建时可以命名.这些名字会在例外失败时显示.
序列化模拟
模拟在它们生命周期的任何时间都可以被序列化.然而,有一些明显的约束:
- 所有常用的匹配器可以被序列化(所有EasyMock匹配器)
- 记录的参数可以被序列化
多线程
默认,模拟不是线程安全的.它们也检查它们是仅在一个线程被使用.为了同步它,调用makeThreadSafe方法. 注意所有IMocksControl创建的模拟和另一个将会是同步的.
向后兼容
EasyMock 2 包含了一个兼容层,所以使用在Java1.5上EasyMock 1.2不需要任何修改就可以工作.仅当失败方式时已知的不同才可见:在失败消息和堆栈上有一些小改变.失败报告使用Java的AssertionError替代了JUnit的 AssertionFailedError.
EasyMock 2.1 介绍了一个反馈特性,在EasyMock 2.2已经被移除,因为它太复杂.自从EasyMock 2.2, IAnswer接口为反馈提供了这个功能.
相关日志
本站文章除特别标示外,其他文章都属于原创内容,转载请按以下格式注明:
本文来源:舞命小丢
原文链接:http://thinking.5ming.org.cn/2008/10/27/easymock-read-me/
TrackBack:http://thinking.5ming.org.cn/2008/10/27/easymock-read-me/trackback/
