安装:
要求:
- java1.5或以上
- 依赖包:Cglib (2.2+) 和 Objenesis (2.0+)
使用MAVEN
Maven的中心仓库有EasyMock ,只需要添加如下的依赖到pom.xml:
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.3.1</version>
<scope>test</scope>
</dependency>
独立部署
1.下载EasyMock zip文件
2.将easymock.jar
添加到classpath
3.想使用mocking,还要添加Objenesis和Cglib到classpath
4.下载的文件中还包含了javadoc、tests、sources和samples的jar包
安卓(3.2以后)
EasyMock可以在Android VM (Dalvik)上使用。仅需要将EasyMock和Dexmaker作为依赖添加到你要测试的apk项目中就可以了。Dexmaker代替了Cglib。如果你使用Maven,最后需要的依赖如下所示:
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.3.1</version>
<exclusions>
<exclusion>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.dexmaker</groupId>
<artifactId>dexmaker</artifactId>
<version>1.1</version>
</dependency>
Mocking
现在我们要创建一个test用例和玩具程序来理解EasyMock的功能。你也可以参考 samples 和 Getting Started.。我们的第一个test是要检查删除一个不存在的文件是否不会通知给collaborator,下面是不使用Mock对象的test
import org.junit.*;
public class ExampleTest {
private ClassUnderTest classUnderTest;
private Collaborator mock;
@Before
public void setUp() {
classUnderTest = new ClassUnderTest();
classUnderTest.setListener(mock);
}
@Test
public void testRemoveNonExistingDocument() {
// This call should not lead to any notification
// of the Mock Object:
classUnderTest.removeDocument("Does not exist");
}
}
在测试中经常使用到EasyMock的静态方法,我们可以使用静态导入org.easymock.EasyMock。
import static org.easymock.EasyMock.*;
import org.junit.*;
public class ExampleTest {
private ClassUnderTest classUnderTest;
private Collaborator mock;
}
要获取Mock对象,我们需要
- 为我们想要模拟的接口创建一个Mock对象
- 记录预期的行为
- 切换Mock对象到回放状态
下面是第一个例子:
@Before
public void setUp() {
mock = mock(Collaborator.class); // 1
classUnderTest = new ClassUnderTest();
classUnderTest.setListener(mock);
}
@Test
public void testRemoveNonExistingDocument() {
// 2 (we do not expect anything)
replay(mock); // 3
classUnderTest.removeDocument("Does not exist");
}
在步骤3激活以后,mock是一个Collaborator
接口的Mock对象,不期望任何调用。这意味着如果我们更改ClassUnderTest
去调用任何接口的方法,Mock对象都会抛出一个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)
...
使用注解(3.2版本以后)
使用注解可以以一种更友好、更短的方式去创建mock对象病将mock对象注入到被测试的class中。下面是上面的例子使用注解后的版本:
import static org.easymock.EasyMock.*;
import org.easymock.EasyMockRunner;
import org.easymock.TestSubject;
import org.easymock.Mock;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(EasyMockRunner.class)
public class ExampleTest {
@TestSubject
private ClassUnderTest classUnderTest = new ClassUnderTest(); // 2
@Mock
private Collaborator mock; // 1
@Test
public void testRemoveNonExistingDocument() {
replay(mock);
classUnderTest.removeDocument("Does not exist");
}
}
对象mock在步骤1被runner实例化。然后在步骤2被runner设置给
ClassUnderTest
的
listenner属性。因为初始化的功能被runner承包了,所以setUp方法可以去掉了。
除此之外,从EasyMock3.3开始,如果你需要另一个ruuner,使用JUnit的rule也是可以的。选择哪一种方式是个人口味的问题。
import static org.easymock.EasyMock.*;
import org.easymock.EasyMockRule;
import org.easymock.TestSubject;
import org.easymock.Mock;
import org.junit.Rule;
import org.junit.Test;
public class ExampleTest {
@Rule
public EasyMockRule mocks = new EasyMockRule(this);
@TestSubject
private ClassUnderTest classUnderTest = new ClassUnderTest();
@Mock
private Collaborator mock;
@Test
public void testRemoveNonExistingDocument() {
replay(mock);
classUnderTest.removeDocument("Does not exist");
}
}
注解有一个可选元素,“type”,用来描述mock是一个“nice” mock或者是"strict" mock. 另一个可选的注解,"name"可以为mock命名,在调用mock()方法的时候会用到,比如会出现在预期失败的提示信息里面。最后一个可选的元素视"fieldName",用来说明mock要注入的目标属性的名字。Mock实例可以注入到任何@TestSubject类型匹配的属性中。如果不止一个mock可以匹配到相同的目标类属性上就会被认为是error。这种场景下,用fieldName
修饰可以避免这种模棱两可的匹配。
@Mock(type = MockType.NICE, name = "mock", fieldName = "someField")
private Collaborator mock;
@Mock(type = MockType.STRICT, name = "anotherMock", fieldName = "someOtherField")
private Collaborator anotherMock;
EasyMockSupport
EasyMockSupport
的存在是为了帮助你跟踪有的mock。你的测试用例可以继承它。它会自动的批量注册所有已经创建的mock实例并回放,重置或者是验证而无需明确的操作每一个mock。下面是例子:
public class SupportTest extends EasyMockSupport {
private Collaborator firstCollaborator;
private Collaborator secondCollaborator;
private ClassTested classUnderTest;
@Before
public void setup() {
classUnderTest = new ClassTested();
}
@Test
public void addDocument() {
// creation phase
firstCollaborator = mock(Collaborator.class);
secondCollaborator = mock(Collaborator.class);
classUnderTest.addListener(firstCollaborator);
classUnderTest.addListener(secondCollaborator);
// recording phase
firstCollaborator.documentAdded("New Document");
secondCollaborator.documentAdded("New Document");
replayAll(); // replay all mocks at once
// test
classUnderTest.addDocument("New Document", new byte[0]);
verifyAll(); // verify all mocks at once
}
}
Strict Mocks
通过EasyMock.mock()
方法反悔的Mock实例,不会检查方法调用的顺序。如果你想生成对方法调用顺序检查的strict Mock,用
EasyMock.strictMock()
去创建。
如果在strict Mock对象上调用了一个不被预期的方法,异常的信息将会显示第一次引起冲突的方法调用的点上。verify(mock)
会显示所有漏掉的方法调用。
Nice Mocks
对于使用mock()
方法生成的Mock对象,所有非预期的方法调用都会抛出AssertionError
异常。用niceMock()
代替mock()生成的mock对象则默认可以调用所有方法,并返回一个合适的空值(0
,null
orfalse
)。
Partial mocking
有时,你只想mock一个类的一部分方法,而另一部门保持原有类的行为。这种情况经常发生在你想测试一个方法,而这个方法要调用同一个类中的另外一个方法时。所以你想保留被测试方法的原有行为,而mock其他方法的行为。
在这种情况下,应该做的第一件事是思考重构,因为大部分情况下问题出在设计缺陷上。如果不是设计缺陷或者由于开发约束你不能这样做的时候,可以如下解决:
ToMock mock = partialMockBuilder(ToMock.class)
.addMockedMethod("mockedMethod").createMock();
在这个例子中,只有通过
添加的方法才会被mockaddMockedMethod(s)
(例子中的
mockedMethod()
)。其他方法还会保持原有的行为。一种例外情况:抽象方法默认是被mock的。
反回一个partialMockBuilder
的方法。可以看一下javadoc。IMockBuilder接口。它包含了一些简单创建
partial moc
k
备注:EasyMock为Object的方法
(equals, hashCode, toString, finalize)
提供了默认实现。然而,对于 partial mock
,如果这些方法没有被明确的mock,它们会保持原有的行为而不是EasyMock提供的默认行为。
自测试
你可以通过调用构造器来创建mock。当你想要测试一个类原本的方法,mock该类的其他方法时会很方便。如下所示:
ToMock mock = partialMockBuilder(ToMock.class)
.withConstructor(1, 2, 3); // 1, 2, 3 are the constructor parameters
可以查看example中的ConstructorCalledMockTest
。
代替默认的类实例化方式
由于某种原因(通常是JVM不支持),EasyMock不能在你的环境中对class进行mock。类是通过工厂模式来实例化的。这种情况下的失败,你可以将默认的实例化替换成:
- 一个很好的旧版本中的类DefaultClassInstantiator可以很好的实例化经过序列化的类,另外还会尽力选用最合适的构造器和参数。
- 仅通过实现接口IClassInstantiator来获取实例。
你可以使用ClassInstantiatorFactory.setInstantiator()来设置新实例。你也可以使用setDefaultInstantiator()来设置回默认值。
重要:实例是静态的,所以在你的单元测试中是共享的。确保在必要的时候进行reset操作。
序列化类的mock
类的mock也可以被序列化。然而,因为它继承了一个可序列化的类,这个类可能已经通过比如writeObject方法定义了特殊的行为。这些方法在序列化mock的时候仍然会被调用并可能引起失败。这种情况,可通过调用构造器来创建mock进行变通处理。
尽管如此,在另外一个class loader反序列化mock实例然后再序列化可能会失败,这种情况没有经过测试。
mocking类的限制
- 对接口进行mock,EasyMock提供了一个内置的行为,比如
equals()
,toString()
,hashCode()
和finalize(),对类的mock也一样。这也意味着你无法记录你自己提供的这些方法的行为。这个限制是EasyMock的一个失误,因为它妨碍你去关心这些方法。
- final方法不能被mock。如果进行调用,他们原有的逻辑会被执行
私有方法不能被mock。如果对私有方法进行调用,那么原本的行为会被执行。在partial mocking中,如果你测试的方法调用了其他的私有方法,你需要进行额外的测试,因为你不能mock那些被调用的私有方法。
类的实例化使用Objenesis控制。查看支持的JVM列表here.
给Mock对象命名
可以使用mock(String name, Class<T> toMock)
,strictMock(String name, Class<T> toMock)
或者 niceMock(String name, Class<T> toMock)方法创建Mock对象,并给Mock对象命名。Mock的名字会显示在异常信息中。
行为
第二个测试
我们来写第二个测试。如果在被测试的类中添加文件,我们预期会调用mock对象的mock.documentAdded()
方法,参数是文档的标题。
@Test
public void testAddDocument() {
mock.documentAdded("New Document"); // 2
replay(mock); // 3
classUnderTest.addDocument("New Document", new byte[0]);
}
在记录的状态中(调用 replay之前
),Mock对象并没有Mock对象的行为,但是它记录了方法的调用。在调用replay
之后,它就有了Mock对象的行为,会检查预期的方法是否真的被调用。
如果classUnderTest.addDocument("New Document", new byte[0])
调用了预期的方法,但是使用了错误的参数,Mock对象会抛出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)
...
所有遗漏的预期调用都会被显示,不被预期的方法调用也会被显示。如果方法调用执行的次数过多,Mock对象也会抛出异常:
java.lang.AssertionError:
Unexpected method call documentAdded("New Document"):
documentAdded("New Document"): expected: 1, actual: 2
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)
...
改变相同方法调用的行为
方法的行为是可以被改变的。方法的调用次数,返回值,抛出的异常可以链式调用。举个例子,我们对voteForRemoval("Document")
方法定义如下:
- 前三次调用返回42
- 接下来的四次调用抛出
RuntimeException
- 接下来返回一次-42
expect(mock.voteForRemoval("Document"))
.andReturn((byte) 42).times(3)
.andThrow(new RuntimeException(), 4)
.andReturn((byte) -42);
改变EasyMock默认的行为
-
EasyMock支持通过配置property来改变它的行为。
主要的目的是允许在新版本中使用旧版本遗留的行为。目前支持的属性包括:
easymock.notThreadSafeByDefault
-
如果为true,mock不是线程安全的。默认为false。
easymock.enableThreadSafetyCheckByDefault
如果为true,默认会打开线程安全检查特性。默认值为false。
easymock.disableClassMocking
不允许对类进行mocking(只允许接口的mocking)。默认是false。
以上属性可以通过两种方式设置:
- classpath默认包的
easymock.properties
文件 - 调用
EasyMock.setEasyMockProperty
方法。EasyMock
类中的常量是可用的。在编码中设置属性会覆盖easymock.properties
文件中设置的属性。
Object方法
对于EasyMock创建的Mock对象,Object的四个方法equals()
, hashCode()
,toString()
andfinalize()
的行为不能被改变,即使他们是Mock对象所实现的接口中的一部分。
为方法使用Stub行为
有时,我们希望我们的Mock对象响应一些方法调用,但是我们不想检查该方法被调用几次,甚至是有没有被调用。stub行为可以使用方法sandStubReturn(Object value)
,andStubThrow(Throwable throwable)
,andStubAnswer(IAnswer<T> answer)
和asStub()
来定义。下面的代码把Mock对象构造成调用voteForRemoval("Document")
返回一次42,其它参数返回-1.
expect(mock.voteForRemoval("Document")).andReturn(42);
expect(mock.voteForRemoval(not(eq("Document")))).andStubReturn(-1);
重用Mock对象
Mock Objects may be reset by reset(mock)
.
If needed, a mock can also be converted from one type to another by calling resetToNice(mock)
, resetToDefault(mock)
ou resetToStrict(mock)
.
Mock对象可以通过reset(mock)
重置。
如果需要的话,mock可以通过调用方法resetToNice(mock)
, resetToDefault(mock)
ouresetToStrict(mock)
.从一种类型转换为另一种类型。
验证
第一个验证
到目前,我们还有一个error没有处理。如果我们定义了行为,我们希望验证它真的起作用了。目前的test如果Mock的方法没有被调用是可以通过的。为了验证定义的行为被使用了,我们必须要调用verify(mock)
。
@Test
public void testAddDocument() {
mock.documentAdded("New Document"); // 2
replay(mock); // 3
classUnderTest.addDocument("New Document", new byte[0]);
verify(mock);
}
现在,如果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)
...
异常的信息列出了所有遗漏的预期调用。
预期指定次数的调用
到目前为止,我们的test只考虑了一个方法的调用。下面的test会检查添加一个已经存在的文件会通过合适的参数调用mock.documentChanged()
方法。为了确保效果,我们对此检查3次。
@Test
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)
定义调用次数。代码变成如下形式:
@Test
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: 4
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)
抛出如下异常。
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)
来包装预期的方法调用,用andReturn(Object returnValue)
来定义返回值,andReturn(Object returnValue)
是通过调用expect(T value)
返回的对象的方法
举例说明,我们检查删除文档的工作流。如果ClassUnderTest
的删除文件函数被调用,它会询问所有的collaborators,collaborators会通过调用byte voteForRemoval(String title)
来对删除文件操作进行投票。正直则删除文件,文件会被删除,并且所有的collaborators都会调用documentRemoved(String title)
方法。
@Test
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);
}
@Test
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");
取代调用expect(T value)
来获取对象并设置返回值,我们可以调用expectLastCall()
来返回对象。换成如下形式:
expect(mock.voteForRemoval("Document")).andReturn((byte) 42);
我们还可以使用:
mock.voteForRemoval("Document");
expectLastCall().andReturn((byte) 42);
这种定义方式只应该在代码太长的时候使用,因为它不支持编译器的类型检查。
处理异常
定义抛出的exceptions(更准确的说是Throwables),通过expectLastCall()
或者expect(T
value
)返回的对象提供了方法andThrow(Throwable throwable)
。
未检查异常(也就是RuntimeException
,Error
和所有他们的子类)可能从任何一个方法抛出。检查型异常只能从真正抛出他们的方法中抛出。
创建返回值或异常
有时我们希望我们的mock对象在实际调用的时候返回一个值或者是抛出一个异常。从EasyMock2.2版本开始,通过expectLastCall()
和expect(T value)
返回的mock对象提供了
andAnswer(IAnswer answer)
方法,该方法允许定义IAnswer
的实现类用来创造返回值或者是异常。
在IAnswer
回调方法的内部,通过EasyMock.getCurrentArguments()
可以得到调用mock方法的参数。如果你这样使用,像重新定义参数或者改变参数顺序的重构都会破坏你的用例,以一定要小心使用
另一个替代IAnswer
的选择是andDelegateTo
和andStubDelegateTo
方法。他们允许将方法调用委托给一个被mock的接口的实现,并由这个实现提供答案。赞成的观点是,在IAnswer
中通过EasyMock.getCurrentArguments()
获取调用参数现在变成了通过实现类的方法调用。这是重构安全的。反对者的观点是你不得不手动的提供一个mock实现...这正是你想用EasyMock用力避免的事情。如果接口有很多方法的话也比较让人苦恼。最后,mock的类型和实现类的类型不能得到静态的检查。如果出于某种原因,实现类没有实现被委托的方法。你会得到异常。然而,这种情况是非常少的。
为了正确理解这两种选择,这里有一个例子:
List<String> l = mock(List.class);
// andAnswer style
expect(l.remove(10)).andAnswer(new IAnswer<String>() {
public String answer() throws Throwable {
return getCurrentArguments()[0].toString();
}
});
// andDelegateTo style
expect(l.remove(10)).andDelegateTo(new ArrayList<String>() {
@Override
public String remove(int index) {
return Integer.toString(index);
}
});
检查多个Mock对象方法的调用顺序
到目前为止,我们看到的都是通过EasyMock
的静态方法构造的一个单独的mock对象。但是,大部分这些静态方法只是隐式的控制mock对象并对该对象进行委托。Mock对象的控制是IMocksControl
接口的实现。
所以,下面的代码
IMyInterface mock = strictMock(IMyInterface.class);
replay(mock);
verify(mock);
reset(mock);
可以替换为
IMocksControl ctrl = createStrictControl();
IMyInterface mock = ctrl.createMock(IMyInterface.class);
ctrl.replay();
ctrl.verify();
ctrl.reset();
IMocksControl允许创建多个Mock对象,所以它可以检查多个mock对象的方法调用顺序。举例说明,我们创建两个IMyInterface
的mock,然后我们预期按照mock1.a()
和mock2.a()的顺序进行调用,然后
任意次数的调用mock1.c()
和mock2.c()
,最后按照mock2.b()
和 mock1.b()
的顺序进行调用。
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();
放松调用次数
放松预期方法的调用次数,还有几个额外的方法用来代替times(int count)
- 预期调用min到max次
-
最少一次
- 任意次
times(int min, int max)
atLeastOnce()
anyTimes()
如果没有定义预期次数,默认为一次。如果准确的表述可以用once()
或者 times(1)
控制方法调用顺序检查的开关
有的时候,我们只想检查Mock对象一部分方法调用的顺序。在记录阶段,你可以调用checkOrder(mock, true)
打开顺序检查,调用checkOrder(mock, false)
关闭顺序检查
strict Mock 对象和normal Mock 对象:有两个区别:
- strict Mock对象在创建之后有顺序检查
- strict Mock对象在调用reset后有顺序检查
灵活的参数匹配预期
为了匹配Mock对象的一个预期的方法的调用,对象的参数默认通过equals()
来比较。但是这样会导致问题。比如,我们考虑下面的预期:
String[] documents = new String[] { "Document 1", "Document 2" };
expect(mock.voteForRemovals(documents)).andReturn(42);
如果方法调用使用了另外一个包含同样内容的数组,我们会得到一个exception,因为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)
...
为了指定方法调用使用相同的数组,我们需要使用EasyMock
的静态方法aryEq
来进行检查
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()
,
anyObject(Class clazz)
,
anyShort()
,
anyString()
匹配任何值,对于所有的基础数据类型有效。
eq(X value, X delta)
匹配实际的值和给出的值是否想能,对于float和double有效
aryEq(X value)
通过调用
Arrays.equals()
判断实际值和给出的值是否相同。对于基础数据类型数组有效。
isNull()
,
isNull(Class clazz)
判断实际值是否为空。
notNull()
,
notNull(Class clazz)
判断实际值是否非空
same(X value)
判断实际值是否和给定的值为同一个对象
isA(Class clazz)
判断实际值是否是一个接口或类,或者是否为一个给定类的子接口或子类。Null总是返回false。
lt(X value)
,
leq(X value)
,
geq(X value)
,
gt(X value)
判断实际值是否小于、小于等于、大于等于、大于给定的值。对于基础数据类型和实现了
Comparable
的对象有效
startsWith(String prefix), contains(String substring), endsWith(String suffix)
判断实际值是否开始、包含、结束给定值,对于String类型有效。
matches(String regex), find(String regex)
判断实际值或子字符串是否匹配给定的正则表达式。对于String类型有效。
and(X first, X second)
判断实际值是否同时满足first和second条件。对于基础数据类型有效。
or(X first, X second)
判断实际值是否满足first或second条件。对于基础数据类型有效。
not(X value)
判断实际值是否和给定值不匹配
cmpEq(X value)
判断实际值和给定值是否相同,通过
Comparable.compareTo(X o)
进行比较。对于数字类型的基础数据类型和实现了
Comparable
接口的类有效
cmp(X value, Comparator<X> comparator, LogicalOperator operator)
判断实际值和匹配值是否满足
comparator.compare(actual, value)
方法的比较结果,
operator
可以是<,<=,>,>= or ==。对于对象有效
定义自己的参数匹配规则
有时,我们希望定义自己的参数匹配规则。比如说你要定义一个exception的参数匹配规则是参数类型和message都相同。应该使用下面的方法:
IllegalStateException e = new IllegalStateException("Operation not allowed.")
expect(mock.logThrowable(eqException(e))).andReturn(true);
通过两个步骤可以达到:定义新的参数匹配规则,声明静态方法eqException
。
定义新的参数匹配规则,我们通过实现接口org.easymock.IArgumentMatcher
来实现。这个接口包含了两个方法:matches(Object actual)
用来检查实际的参数和给定的参数是否匹配,另一个方法appendTo(StringBuffer buffer)
将参数匹配的描述追加到给定的string buffer上。实现如下:
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创建参数匹配规则,通过EasyMock的静态方法reportMatcher(IArgumentMatcher matcher)
报告给EasyMock并且返回一个可能用在方法调用内部的值(典型的如:0
,null
orfalse
).第一个尝试看起来如下:
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 extends Throwable>T eqException(T in) {
reportMatcher(new ThrowableEquals(in));
return null;
}