Let's EasyMock

入职之后开发和之前自己的project或者在startup的一个明显不同是,colleague的code review和开发的整个生命周期的维护(设计,实现,测试,qa,部署,AB test,logging)。

在单元测试时,由于很多类的功能都是依赖于其他类的功能,如果在测试一个类是全部引入其依赖的类的对象,会导致高度耦合和复杂度,首先一个问题是,单元测试失败时我们无法定位是哪里的问题,是当前正在测试的类的问题还是其依赖的类的问题。

单元测试的目的是检测单个function的逻辑是否能工作正常,而不是各个类耦合在一起能否一起完成整体功能(有intergration测试)。所以我们需要模拟依赖的各个类,使得其确定能工作正常(设定其行为)。

EasyMock就是一个经常使用的Mock工具,我们可以借助其快速生成我们想要的mock object。

Installation

Maven

1
2
3
4
5
6
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.4</version>
<scope>test</scope>
</dependency>

Mocking

The first Mock Object

假设我们现在有下面的test class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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");
}
}

对于大部分的mock,我们只需要静态import EasyMock的方法

1
2
3
4
5
6
7
import static org.easymock.EasyMock.*;
import org.junit.*;

public class ExampleTest {
private ClassUnderTest classUnderTest;
private Collaborator mock;
}

对于创建我们所需要的Mock object,我们只需要:

  1. 创建mock object
  2. 设定我们希望的mock的行为(function及其返回结果)
  3. 切换对象到replay状态

创建mock object我们可以使用mock方法或者annotations:

1
2
3
4
5
6
7
8
9
10
11
12
13
@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");
}

在上面的测试中,我们没有指定任何mock的行为,然后replay it。也就是我们expect no calls,所以在第三步时我们call any of the interface methods时,会得到AssertionError。

1
2
3
4
5
6
7
8
9
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)
...

Using annotations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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会由runner初始化,可以不用添加setUp方法来初始化mock。

然后有时我们需要其他的runner,这时我们可以JUnit rule来代替runner。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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");
}
}

另外annotation可以下面的optional的element:

  • type,用于指定mock的类型(nice, strict)。
  • name,用于指定mock名字。
  • fieldName,用于specifying the target field name where the mock should be injected.
1
2
3
4
5
@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,自动的注册,replay以及verfiy这些mocks,可以省去显示进行这些步骤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

在使用mock()创建的mock对象时(默认TYPE),方法调用以及方法调用的次数会被check,但是方法调用的顺序并不会被check,如果如果需要,可以使用EasyMock.strictMock()创建mock对象。

如果调用了mock对象所没有的方法或者不是调用当前顺序位置的方法,strict mock对象会抛出异常并show the method calls expected at this point followed by the first conflicting one。verify(mock)会列出所有缺失的method calls。

Nice mocks

默认的mock()创建的mock对象在遇到unexpected method calls会抛出AssertionError。如果需要创建一个遇到unexpected method时返回合适的空值(0, null, false),可以使用niceMock()

Partial mocking

有时我们需要只mock类的部分方法,而keep the normal behavior of other methods,特别是我们需要测试一个方法which调用其他在同一个类的方法。

1
2
ToMock mock = partialMockBuilder(ToMock.class)
.addMockedMethod("mockedMethod").createMock();

Selt testing

1
2
ToMock mock = partialMockBuilder(ToMock.class)
.withConstructor(1, 2, 3); // 1, 2, 3 are the constructor parameters

Using Stub Behavior for Methods

1
2
expect(mock.voteForRemoval("Document")).andReturn(42);
expect(mock.voteForRemoval(not(eq("Document")))).andStubReturn(-1);

Resuing mock object

可以使用reset(mock)来reset mock.
另外还有resetwToNice(mock), resetToDefault(mock), resetToStrict(mock)来reset到其他模式。

Behavior

A second test

假设我们有如下的test:

1
2
3
4
5
6
@Test
public void testAddDocument() {
mock.documentAdded("New Document"); // 2
replay(mock); // 3
classUnderTest.addDocument("New Document", new byte[0]); // will call mock.documentAdded()
}

Through the second, we expect a call to the mock.documentAdded() on the mock Object with the title of the document as argument.

在record state(记录状态下,也就是在调用replay之前),mock并不会真正表现像其mock的object,而只是记录所有这个mock对象的method calls。在调用了replay之后,mock对象才真正具有mock object的行为,也就是会check是否方法正确被调用了。

这时如果调用mock object不存在的方法或者with wrong argument,或者调用次数不正确,mock object会抛出一个AssertionError:

1
2
3
4
5
6
7
8
9
10
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 object同样会抛出exception(默认是调用一次)。

1
2
3
4
5
6
7
8
9
10
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)
...

** Changing Behavior for the Same Method Call

It is also possible to specify a changing behavior for a method. The method times, andReturn, andThrow may be chained. As an example, we define voteForRemoval("Document") to

  • return 42 for the first three calls,
  • throw a RuntimeException for the next four calls,
  • return -42 once.
1
2
3
4
expect(mock.voteForRemoval("Document"))
.andReturn((byte) 42).times(3)
.andThrow(new RuntimeException(), 4)
.andReturn((byte) -42);

Altering EasyMock default behavior

EasyMock provides a property mechanisim allowing to alter its behavior. It mainly aims at allowing to use a legacy behavior on a new version. Currently supported properties are:

easymock.notThreadSafeByDefault
If true, a mock won’t be thread-safe by default. Possible values are “true” or “false”. Default is false

easymock.enableThreadSafetyCheckByDefault
If true, thread-safety check feature will be on by default. Possible values are “true” or “false”. Default is false

easymock.disableClassMocking
Do not allow class mocking (only allow interface mocking). Possible values are “true” or “false”. Default is false.
Properties can be set in two ways.

In an easymock.properties file set in the classpath default package
By calling EasyMock.setEasyMockProperty. Constants are available in the EasyMock class. Setting properties in the code obviously override any property set in easymock.properties

** Using Stub Behavior for Methods

Sometimes, we would like our Mock Object to respond to some method calls, but we do not want to check how often they are called, when they are called, or even if they are called at all. This stub behavoir may be defined by using the methods andStubReturn(Object value), andStubThrow(Throwable throwable), andStubAnswer(IAnswer<T> answer) and asStub(). The following code configures the MockObject to answer 42 to voteForRemoval("Document") once and -1 for all other arguments:

1
2
expect(mock.voteForRemoval("Document")).andReturn(42);
expect(mock.voteForRemoval(not(eq("Document")))).andStubReturn(-1);

** Reusing a Mock Object

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) or resetToStrict(mock).

Verification

A first verification

到目前为止,我们知道如何设定mock的behavior,但是我们还不知道这些指定的behavior有没有被执行,如果没有被执行,test只是会简单的被pass。如果要verify这些specified behavior是否被执行了,以及是否被执行了对应的次数,我们可以调用verify(mock)

1
2
3
4
5
6
7
@Test
public void testAddDocument() {
mock.documentAdded("New Document"); // 2
replay(mock); // 3
classUnderTest.addDocument("New Document", new byte[0]);
verify(mock);
}

如果指定的方法没有被执行,我们会得到下面的exception:

1
2
3
4
5
6
7
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)
...

message会列出所有missing的expected calls。

Expecting an Explict Number of Calls

1
2
3
4
5
6
7
8
9
10
11
12
13
@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);
}

我们可以使用time(N)来代替声明多次方法调用:

1
2
3
4
5
6
7
8
9
10
11
12
@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);
}

如果method调用太多次,则会得到exception:

1
2
3
4
5
6
7
8
9
10
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)
...

如果调用次数太少,同样得到exception

1
2
3
4
5
6
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)

Specifying Return Values

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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);
}

返回类型在编译的时候会被检查。
两种指定andReturn的方式:

1
expect(mock.voteForRemoval("Document")).andReturn((byte) 42);
1
2
mock.voteForRemoval("Document");
expectLastCall().andReturn((byte) 42);

Working with Exceptions

For specifying exceptions (more exactly: Throwables) to be thrown, the object returned by expectLastCall() and expect(T value) provides the method andThrow(Throwable throwable). The method has to be called in record state after the call to the Mock Object for which it specifies the Throwable to be thrown.

Unchecked exceptions (that is, RuntimeException, Error and all their subclasses) can be thrown from every method. Checked exceptions can only be thrown from the methods that do actually throw them.

Creating Return Values or Exceptions

前面的expected result都是在设计测试就已经确定。如果需要某个方法测试的返回结果根据传入参数或者其他因素变化时,我们需要使用andAnswer(IAnswer answer)。借助实现IAnswer接口我们可以设定如何得到returned value。EasyMock.getCurrentArguments()可以得到测试方法的输入参数。

An alternative to IAnswer are the andDelegateTo and andStubDelegateTo methods. They allow to delegate the call to a concrete implementation of the mocked interface that will then provide the answer. The pros are that the arguments found in EasyMock.getCurrentArguments() for IAnswer are now passed to the method of the concrete implementation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}
});

Checking Method Call Order Between Mocks

在只有一个mock object,如果需要check方法调用的顺序:

1
2
3
4
IMyInterface mock = strictMock(IMyInterface.class);
replay(mock);
verify(mock);
reset(mock);

我们还可以这样实现:

1
2
3
4
5
IMocksControl ctrl = createStrictControl();
IMyInterface mock = ctrl.createMock(IMyInterface.class);
ctrl.replay();
ctrl.verify();
ctrl.reset();

第二种方法的优势,当有多个mock object时,需要check多个mock object的方法调用顺序可以使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();

Relaxing Call Counts

To relax the expected call counts, there are additional methods that may be used instead of times(int count):

1
times(int min, int max)

to expect between min and max calls,

1
atLeastOnce()

to expect at least one call, and

1
anyTimes()

to expected an unrestricted number of calls.

If no call count is specified, one call is expected. If we would like to state this explicitely, once() or times(1) may be used.

Switching Order Checking On and Off

Sometimes, it is necessary to have a Mock Object that checks the order of only some calls. In record phase, you may switch order checking on by calling checkOrder(mock, true) and switch it off by calling checkOrder(mock, false).

There are two differences between a strict Mock Object and a normal Mock Object:

A strict Mock Object has order checking enabled after creation.
A strict Mock Object has order checking enabled after reset (see Reusing a Mock Object).

** Flexible Expectations with Argument Matchers

在指定expected行为时,我们可能希望其行为根据输入参数改变而改变。
默认的expeced的参数是使用equals()匹配的,也就是只有当实际调用时的参数equalsexpect时设置的参数才会得到expected的行为,否则则不是expeced的调用,会得到AssertionError。

1
2
String[] documents = new String[] { "Document 1", "Document 2" };
expect(mock.voteForRemovals(documents)).andReturn(42);

如果有下面调用:

1
2
String[] documentsArgs = new String[] {"doc1", "doc2"};
mock.voteForRemovals(documentsArgs);

会得到:

1
2
3
4
5
6
7
8
9
10
11
12
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)
...

除了默认的equals的参数匹配器之外,EasyMock还提供了其他许多参数匹配器:

1
eq(X value)

Matches if the actual value is equals the expected value. Available for all primitive types and for objects.

1
anyBoolean(), anyByte(), anyChar(), anyDouble(), anyFloat(), anyInt(), anyLong(), anyObject(), anyObject(Class clazz), anyShort(), anyString()

Matches any value. Available for all primitive types and for objects.

1
eq(X value, X delta)

Matches if the actual value is equal to the given value allowing the given delta. Available for floatand double.

1
aryEq(X value)

Matches if the actual value is equal to the given value according to Arrays.equals(). Available for primitive and object arrays.

1
isNull(), isNull(Class clazz)

Matches if the actual value is null. Available for objects.

1
notNull(), notNull(Class clazz)

Matches if the actual value is not null. Available for objects.

1
same(X value)

Matches if the actual value is the same as the given value. Available for objects.

1
isA(Class clazz)

Matches if the actual value is an instance of the given class, or if it is in instance of a class that extends or implements the given class. Null always return false. Available for objects.

1
lt(X value), leq(X value), geq(X value), gt(X value)

Matches if the actual value is less/less or equal/greater or equal/greater than the given value. Available for all numeric primitive types and Comparable.

1
startsWith(String prefix), contains(String substring), endsWith(String suffix)

Matches if the actual value starts with/contains/ends with the given value. Available for Strings.

1
matches(String regex), find(String regex)

Matches if the actual value/a substring of the actual value matches the given regular expression. Available for Strings.

1
and(X first, X second)

Matches if the matchers used in first and second both match. Available for all primitive types and for objects.

1
or(X first, X second)

Matches if one of the matchers used in first and second match. Available for all primitive types and for objects.

1
not(X value)

Matches if the matcher used in value does not match.

1
cmpEq(X value)

Matches if the actual value is equals according to Comparable.compareTo(X o). Available for all numeric primitive types and Comparable.

1
cmp(X value, Comparator<X> comparator, LogicalOperator operator)

Matches if comparator.compare(actual, value) operator 0 where the operator is <,<=,>,>= or ==. Available for objects.

1
capture(Capture<T> capture), captureXXX(Capture<T> capture)

Matches any value but captures it in the Capture parameter for later access. You can do and(someMatcher(...), capture(c)) to capture a parameter from a specific call to the method. You can also specify a CaptureType telling that a given Capture should keep the first, the last, all or no captured values.

另外需要注意的是,传入参数为基本类型时而允许任何值时,需要使用EasyMock.anyLong(),而不能用EasyMock.isA(Long.class)

传入参数可能为null时,需要使用or(isA(String.class), isNull())这样的形式,而不能只是isA(String.class)

Defining your own Argument Matchers

除了上面的参数matcher,我们还可以定义自己的参数matcher。

Sometimes it is desirable to define own argument matchers. Let’s say that an argument matcher is needed that matches an exception if the given exception has the same type and an equal message. It should be used this way:

1
2
IllegalStateException e = new IllegalStateException("Operation not allowed.")
expect(mock.logThrowable(eqException(e))).andReturn(true);

Two steps are necessary to achieve this:

  1. The new argument matcher has to be defined,
  2. and the static method eqException has to be declared.

To define the new argument matcher, we implement the interface org.easymock.IArgumentMatcher. This interface contains two methods: matches(Object actual) checks whether the actual argument matches the given argument, and appendTo(StringBuffer buffer) appends a string representation of the argument matcher to the given string buffer. The implementation is straightforward:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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()) &amp;&amp; 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("\"")");
}
}

The method eqException must create the argument matcher with the given Throwable, report it to EasyMock via the static method reportMatcher(IArgumentMatcher matcher), and return a value so that it may be used inside the call (typically 0, null or false). A first attempt may look like:

1
2
3
4
public static Throwable eqException(Throwable in) {
EasyMock.reportMatcher(new ThrowableEquals(in));
return null;
}

However, this only works if the method logThrowable in the example usage accepts Throwables, and does not require something more specific like a RuntimeException. In the latter case, our code sample would not compile:

1
2
IllegalStateException e = new IllegalStateException("Operation not allowed.")
expect(mock.logThrowable(eqException(e))).andReturn(true);

Java 5.0 to the rescue: Instead of defining eqException with a Throwable as parameter and return value, we use a generic type that extends Throwable:

1
2
3
4
public static <T extends Throwable> eqException(T in) {
reportMatcher(new ThrowableEquals(in));
return null;
}

Refer to EasyMock user guide.