DualMock - an EasyMock extension
When I have been using easyMock Mock Objects for testing I often find it helpful to intersperse expectations and test code, for example:
mock.start();
control.replay();
server.handleCommand("START");
control.validate();
control.reset();
mock.shutdown();
control.replay();
server.handleCommand("STOP");
control.validate();
Doing this with easyMock requires me to validate and reset the object frequently. I was thinking: What is stopping me from just calling doing this:
mock.start();
server.handleCommand("START");
// call from the object-under-test, mock automatically assumes replay mode.
mock.shutdown();
// Call from the test-harness puts mock back in record mode.
server.handleCommand("STOP");
// Call from object-under-test, mock assumes replay mode again
control.validate();
// Validates the last "STOP" call
Of course, the problem is that the object-under-test (in this case, the server) and the test-code uses the same mock object instance. This lead me to the following solution: Make two mock objects - one for the expectiations and one for the actual code. Thus the name DualMock. Here is a snippet of test code:
public void testImplicitSetVoidCallable() {
DualMockControl control = DualMockControl.createMock(IMethods.class);
IMethods record = (IMethods)control.getRecordMock();
IMethods replay = (IMethods)control.getReplayMock();
record.simpleMethod();
replay.simpleMethod();
record.simpleMethod();
replay.simpleMethod();
control.verify();
}
The code is not completely developed yet, as I have only used it marginally myself. Let me know through email or a comment if you’d like the code.
Dual Mock Test
package org.easymock.tests.dualmock;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import org.easymock.internal.MethodCall;
import org.easymock.internal.OrderedBehavior;
import org.easymock.internal.Range;
import org.easymock.internal.Result;
import org.easymock.tests.IMethods;
public class DualMockTest extends TestCase {
MethodCall lastExpected;
private class SpyOnOrderedBehavior extends OrderedBehavior {
public void addExpected(MethodCall methodCall, Result result, Range count) {
lastExpected = methodCall;
super.addExpected(methodCall, result, count);
}
}
private DualMockControl control =
DualMockControl.createMock(IMethods.class, new SpyOnOrderedBehavior());
private IMethods record = (IMethods)control.getRecordMock();
private IMethods replay = (IMethods)control.getReplayMock();
public void testRecordCall() {
assertNull(lastExpected);
record.simpleMethod();
control.setVoidCallable();
assertNotNull(lastExpected);
assertEquals(lastExpected.getMethod().getName(), "simpleMethod");
}
public void testUnexpectedCall() {
assertThrows("replay.simpleMethod", AssertionFailedError.class, new Runnable() {
public void run() { replay.simpleMethod(); }
});
}
public void testReturnValue() {
record.booleanReturningMethod(5);
control.setReturnValue(false);
boolean result = replay.booleanReturningMethod(5);
assertEquals(false, result);
}
public void testMissingCall() {
record.oneArgumentMethod(true);
control.setReturnValue("test");
assertThrows("mock.verify", AssertionFailedError.class, new Runnable() {
public void run() { control.verify(); }
});
}
public void testImplicitSetVoidCallable() {
record.simpleMethod();
replay.simpleMethod();
record.simpleMethod();
replay.simpleMethod();
control.verify();
}
public void testTwoMethodCalls() {
record.simpleMethod();
record.simpleMethodWithArgument("test");
replay.simpleMethod();
replay.simpleMethodWithArgument("test");
control.verify();
}
public void testOneAndAHalf() {
record.simpleMethod();
replay.simpleMethod();
record.simpleMethod();
assertThrows("mock.verify", AssertionFailedError.class, new Runnable() {
public void run() { control.verify(); }
});
}
public void testWrongParameter() {
record.simpleMethodWithArgument("test1");
assertThrows("wrong parameter", AssertionFailedError.class, new Runnable() {
public void run() { replay.simpleMethodWithArgument("wrong argument"); }
});
}
public void testThrowing() {
record.simpleMethod();
control.setThrowable(new IllegalArgumentException());
assertThrows("throwing method", IllegalArgumentException.class, new Runnable() {
public void run() { replay.simpleMethod(); }
});
}
public void testArgumentWidening() {
byte expected = 12;
record.byteReturningMethod(1);
control.setReturnValue(expected);
assertEquals(expected, replay.byteReturningMethod(1));
}
private void assertThrows(String methodCall, Class expectedException, Runnable runnable) {
try {
runnable.run();
fail("Expected " + methodCall + " to throw " + expectedException);
} catch ( Throwable ex ) {
assertEquals(methodCall + " exception", expectedException, ex.getClass());
}
}
}
Dual Mock Control
package org.easymock.tests.dualmock;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import org.easymock.MockControl;
import org.easymock.internal.IBehavior;
import org.easymock.internal.JavaProxyFactory;
import org.easymock.internal.MethodCall;
import org.easymock.internal.RecordState;
public class DualMockControl {
private class DualReplayHandler implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
recordState.replay();
return behavior.addActual(new MethodCall(method, args)).returnObjectOrThrowException();
}
}
private static final JavaProxyFactory PROXY\_FACTORY = new JavaProxyFactory();
private Object recordMock;
private Object replayMock;
private RecordState recordState;
private final IBehavior behavior;
public DualMockControl(Class classToMock, IBehavior behavior) {
this.behavior = behavior;
this.recordState = new RecordState(behavior);
this.recordMock = PROXY\_FACTORY.createProxy(classToMock, recordState);
this.replayMock = PROXY\_FACTORY.createProxy(classToMock, new DualReplayHandler());
}
public static DualMockControl createMock(Class classToMock, IBehavior behavior) {
return new DualMockControl(classToMock, behavior);
}
public Object getRecordMock() {
return recordMock;
}
public Object getReplayMock() {
return replayMock;
}
public void verify() {
behavior.verify();
}
public void setVoidCallable() {
recordState.setVoidCallable(MockControl.ONE);
}
public void setReturnValue(boolean value) {
recordState.setReturnValue(value, MockControl.ONE);
}
public void setReturnValue(long value) {
recordState.setReturnValue(value, MockControl.ONE);
}
public void setReturnValue(char value) {
recordState.setReturnValue(value, MockControl.ONE);
}
public void setReturnValue(double value) {
recordState.setReturnValue(value, MockControl.ONE);
}
public void setReturnValue(Object value) {
recordState.setReturnValue(value, MockControl.ONE);
}
public void setThrowable(Throwable exception) {
recordState.setThrowable(exception, MockControl.ONE);
}
}
Comments:
[Sravan Kumar] - May 7, 2005
Thanks alot for a nice explanation on Mock. Plz send the entire code stuff and reading materials on dual mock+ easy mock. Look forward hear from you soon.