summaryrefslogtreecommitdiff
path: root/src/com/google/testing/littlemock/LittleMock.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/google/testing/littlemock/LittleMock.java')
-rw-r--r--src/com/google/testing/littlemock/LittleMock.java247
1 files changed, 224 insertions, 23 deletions
diff --git a/src/com/google/testing/littlemock/LittleMock.java b/src/com/google/testing/littlemock/LittleMock.java
index ebd44ab..140f853 100644
--- a/src/com/google/testing/littlemock/LittleMock.java
+++ b/src/com/google/testing/littlemock/LittleMock.java
@@ -30,6 +30,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
/**
@@ -146,14 +147,48 @@ public class LittleMock {
/** Begins a verification step on a mock: the next method invocation on that mock will verify. */
public static <T> T verify(T mock, CallCount howManyTimes) {
+ return verify(mock, howManyTimes, null);
+ }
+
+ private static final class OrderChecker {
+ private MethodCall mLastCall;
+
+ public void checkOrder(List<MethodCall> calls, String fieldName) {
+ MethodCall lastTrial = null;
+ for (MethodCall trial : calls) {
+ if (mLastCall == null || mLastCall.mInvocationOrder < trial.mInvocationOrder) {
+ mLastCall = trial;
+ return;
+ }
+ lastTrial = trial;
+ }
+ fail(formatFailedVerifyOrderMessage(mLastCall, lastTrial, fieldName));
+ }
+
+ private String formatFailedVerifyOrderMessage(MethodCall lastCall, MethodCall thisCall,
+ String fieldName) {
+ StringBuffer sb = new StringBuffer();
+ sb.append("\nCall to:");
+ appendDebugStringForMethodCall(sb, thisCall.mMethod, thisCall.mElement, fieldName, false);
+ sb.append("\nShould have happened after:");
+ appendDebugStringForMethodCall(sb, lastCall.mMethod, lastCall.mElement, fieldName, false);
+ sb.append("\nBut the calls happened in the wrong order");
+ sb.append("\n");
+ return sb.toString();
+ }
+ }
+
+ private static <T> T verify(T mock, CallCount howManyTimes, OrderChecker orderCounter) {
if (howManyTimes == null) {
throw new IllegalArgumentException("Can't pass null for howManyTimes parameter");
}
DefaultInvocationHandler handler = getHandlerFrom(mock);
checkState(handler.mHowManyTimes == null, "Unfinished verify() statements");
+ checkState(handler.mOrderCounter == null, "Unfinished verify() statements");
checkState(handler.mStubbingAction == null, "Unfinished stubbing statements");
checkNoMatchers();
handler.mHowManyTimes = howManyTimes;
+ handler.mOrderCounter = orderCounter;
sUnfinishedCallCounts.add(howManyTimes);
return handler.<T>getVerifyingMock();
}
@@ -273,7 +308,7 @@ public class LittleMock {
return addMatcher(new ArgumentMatcher() {
@Override
public boolean matches(Object value) {
- return (expected == null) ? (value == null) : expected.equals(value);
+ return areEqual(expected, value);
}
}, expected);
}
@@ -464,6 +499,7 @@ public class LittleMock {
public <T> T when(T mock) {
DefaultInvocationHandler handler = getHandlerFrom(mock);
checkState(handler.mHowManyTimes == null, "Unfinished verify() statements");
+ checkState(handler.mOrderCounter == null, "Unfinished verify() statements");
checkState(handler.mStubbingAction == null, "Unfinished stubbing statements");
handler.mStubbingAction = mAction;
sUnfinishedStubbingActions.add(mAction);
@@ -492,12 +528,17 @@ public class LittleMock {
*/
private static final List<ArgumentMatcher> sMatchArguments = new ArrayList<ArgumentMatcher>();
+ /** Global invocation order of every mock method call. */
+ private static final AtomicLong sGlobalInvocationOrder = new AtomicLong();
+
/** Encapsulates a single call of a method with associated arguments. */
private static class MethodCall {
/** The method call. */
private final Method mMethod;
/** The arguments provided at the time the call happened. */
private final Object[] mArgs;
+ /** The order in which this method call was invoked. */
+ private final long mInvocationOrder;
/** The line from the test that invoked the handler to create this method call. */
private final StackTraceElement mElement;
/** Keeps track of method calls that have been verified, for verifyNoMoreInteractions(). */
@@ -507,6 +548,7 @@ public class LittleMock {
mMethod = method;
mElement = element;
mArgs = args;
+ mInvocationOrder = sGlobalInvocationOrder.getAndIncrement();
}
public boolean argsMatch(Object[] args) {
@@ -519,6 +561,20 @@ public class LittleMock {
}
}
+ private static boolean areMethodsSame(Method first, Method second) {
+ return areEqual(first.getDeclaringClass(), second.getDeclaringClass()) &&
+ areEqual(first.getName(), second.getName()) &&
+ areEqual(first.getReturnType(), second.getReturnType()) &&
+ Arrays.equals(first.getParameterTypes(), second.getParameterTypes());
+ }
+
+ private static boolean areEqual(Object a, Object b) {
+ if (a == null) {
+ return b == null;
+ }
+ return a.equals(b);
+ }
+
/**
* Magically handles the invoking of method calls.
*
@@ -565,6 +621,7 @@ public class LittleMock {
* <p>It is reset to null once the verification has occurred.
*/
private CallCount mHowManyTimes = null;
+ private OrderChecker mOrderCounter = null;
/**
* The action to be associated with the stubbed method.
@@ -633,11 +690,12 @@ public class LittleMock {
ArgumentMatcher[] matchers = checkClearAndGetMatchers(method);
StackTraceElement callSite = new Exception().getStackTrace()[2];
MethodCall methodCall = new MethodCall(method, callSite, args);
- innerVerify(method, matchers, methodCall, proxy, callSite, mHowManyTimes);
+ innerVerify(method, matchers, methodCall, proxy, callSite, mHowManyTimes, mOrderCounter);
return defaultReturnValue(method.getReturnType());
} finally {
sUnfinishedCallCounts.remove(mHowManyTimes);
mHowManyTimes = null;
+ mOrderCounter = null;
}
}
});
@@ -681,9 +739,9 @@ public class LittleMock {
* @param operation the name of the operation, used for generating a helpful message
*/
private void checkSpecialObjectMethods(Method method, String operation) {
- if (method.equals(sEqualsMethod)
- || method.equals(sHashCodeMethod)
- || method.equals(sToStringMethod)) {
+ if (areMethodsSame(method, sEqualsMethod)
+ || areMethodsSame(method, sHashCodeMethod)
+ || areMethodsSame(method, sToStringMethod)) {
fail("cannot " + operation + " call to " + method);
}
}
@@ -692,24 +750,25 @@ public class LittleMock {
mRecordedCalls.clear();
mStubbedCalls.clear();
mHowManyTimes = null;
+ mOrderCounter = null;
mStubbingAction = null;
}
private Object innerRecord(Method method, final Object[] args,
MethodCall methodCall, Object proxy, StackTraceElement callSite) throws Throwable {
- if (method.equals(sEqualsMethod)) {
+ if (areMethodsSame(method, sEqualsMethod)) {
// Use identify for equality, the default behavior on object.
return proxy == args[0];
- } else if (method.equals(sHashCodeMethod)) {
+ } else if (areMethodsSame(method, sHashCodeMethod)) {
// This depends on the fact that each mock has its own DefaultInvocationHandler.
return hashCode();
- } else if (method.equals(sToStringMethod)) {
+ } else if (areMethodsSame(method, sToStringMethod)) {
// This is used to identify this is a mock, e.g., in error messages.
return "Mock<" + mClazz.getName() + ">";
}
mRecordedCalls.add(methodCall);
for (StubbedCall stubbedCall : mStubbedCalls) {
- if (stubbedCall.mMethodCall.mMethod.equals(methodCall.mMethod)) {
+ if (areMethodsSame(stubbedCall.mMethodCall.mMethod, methodCall.mMethod)) {
if (stubbedCall.mMethodCall.argsMatch(methodCall.mArgs)) {
methodCall.mWasVerified = true;
return stubbedCall.mAction.doAction(method, args);
@@ -786,44 +845,47 @@ public class LittleMock {
}
private void innerVerify(Method method, ArgumentMatcher[] matchers, MethodCall methodCall,
- Object proxy, StackTraceElement callSite, CallCount callCount) {
+ Object proxy, StackTraceElement callSite, CallCount callCount, OrderChecker orderCounter) {
checkSpecialObjectMethods(method, "verify");
- int total = countMatchingInvocations(method, matchers, methodCall);
+ List<MethodCall> calls = countMatchingInvocations(method, matchers, methodCall);
long callTimeout = callCount.getTimeout();
+ checkState(orderCounter == null || callTimeout == 0, "can't inorder verify with a timeout");
if (callTimeout > 0) {
long endTime = System.currentTimeMillis() + callTimeout;
- while (!callCount.matches(total)) {
+ while (!callCount.matches(calls.size())) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
fail("interrupted whilst waiting to verify");
}
if (System.currentTimeMillis() > endTime) {
- fail(formatFailedVerifyMessage(methodCall, total, callTimeout, callCount));
+ fail(formatFailedVerifyMessage(methodCall, calls.size(), callTimeout, callCount));
}
- total = countMatchingInvocations(method, matchers, methodCall);
+ calls = countMatchingInvocations(method, matchers, methodCall);
}
} else {
- if (!callCount.matches(total)) {
- fail(formatFailedVerifyMessage(methodCall, total, 0, callCount));
+ if (orderCounter != null) {
+ orderCounter.checkOrder(calls, mFieldName);
+ } else if (!callCount.matches(calls.size())) {
+ fail(formatFailedVerifyMessage(methodCall, calls.size(), 0, callCount));
}
}
}
- private int countMatchingInvocations(Method method, ArgumentMatcher[] matchers,
+ private List<MethodCall> countMatchingInvocations(Method method, ArgumentMatcher[] matchers,
MethodCall methodCall) {
- int total = 0;
+ List<MethodCall> methodCalls = new ArrayList<MethodCall>();
for (MethodCall call : mRecordedCalls) {
- if (call.mMethod.equals(method)) {
+ if (areMethodsSame(call.mMethod, method)) {
if ((matchers.length > 0 && doMatchersMatch(matchers, call.mArgs)) ||
call.argsMatch(methodCall.mArgs)) {
setCaptures(matchers, call.mArgs);
- ++total;
+ methodCalls.add(call);
call.mWasVerified = true;
}
}
}
- return total;
+ return methodCalls;
}
private String formatFailedVerifyMessage(MethodCall methodCall, int total, long timeoutMillis,
@@ -926,7 +988,7 @@ public class LittleMock {
}
/** Represents something capable of testing if it matches an argument or not. */
- /*package*/ interface ArgumentMatcher {
+ public interface ArgumentMatcher {
public boolean matches(Object value);
}
@@ -1008,6 +1070,12 @@ public class LittleMock {
return value;
}
+ /** A custom argument matcher, should be used only for object arguments not primitives. */
+ public static <T> T matches(ArgumentMatcher argument) {
+ sMatchArguments.add(argument);
+ return null;
+ }
+
/** Utility method to throw an AssertionError if an assertion fails. */
private static void expect(boolean result, String message) {
if (!result) {
@@ -1073,6 +1141,13 @@ public class LittleMock {
}
}
+ /** Helper method to throw an IllegalStateException if given condition is not met. */
+ private static void checkState(boolean condition) {
+ if (!condition) {
+ throw new IllegalStateException();
+ }
+ }
+
/**
* If the input object is one of our mocks, returns the {@link DefaultInvocationHandler}
* we constructed it with. Otherwise fails with {@link IllegalArgumentException}.
@@ -1092,14 +1167,104 @@ public class LittleMock {
return (DefaultInvocationHandler) invocationHandler;
}
} catch (Exception expectedIfNotAProxyBuilderMock) {}
+ try {
+ // Try with javassist.
+ Class<?> proxyObjectClass = Class.forName("javassist.util.proxy.ProxyObject");
+ Method getHandlerMethod = proxyObjectClass.getMethod("getHandler");
+ Object methodHandler = getHandlerMethod.invoke(mock);
+ InvocationHandler invocationHandler = Proxy.getInvocationHandler(methodHandler);
+ Method getOriginalMethod = invocationHandler.getClass().getMethod("$$getOriginal");
+ Object original = getOriginalMethod.invoke(invocationHandler);
+ if (original instanceof DefaultInvocationHandler) {
+ return (DefaultInvocationHandler) original;
+ }
+ } catch (Exception expectedIfNotJavassistProxy) {}
throw new IllegalArgumentException("not a valid mock: " + mock);
}
+ private static boolean canUseJavassist() {
+ try {
+ Class.forName("javassist.util.proxy.ProxyFactory");
+ return true;
+ } catch (Exception expectedIfNotJavassistProxy) {
+ return false;
+ }
+ }
+
/** Create a dynamic proxy for the given class, delegating to the given invocation handler. */
- private static Object createProxy(Class<?> clazz, InvocationHandler handler) {
+ private static Object createProxy(Class<?> clazz, final InvocationHandler handler) {
+ // Interfaces are simple. Just proxy them using java.lang.reflect.Proxy.
if (clazz.isInterface()) {
return Proxy.newProxyInstance(getClassLoader(), new Class<?>[] { clazz }, handler);
}
+ // Try with javassist.
+ if (canUseJavassist()) {
+ try {
+ Class<?> proxyFactoryClass = Class.forName("javassist.util.proxy.ProxyFactory");
+ Object proxyFactory = proxyFactoryClass.newInstance();
+ Method setSuperclassMethod = proxyFactoryClass.getMethod("setSuperclass", Class.class);
+ setSuperclassMethod.invoke(proxyFactory, clazz);
+ Class<?> methodFilterClass = Class.forName("javassist.util.proxy.MethodFilter");
+ InvocationHandler methodFilterHandler = new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ checkState(method.getName().equals("isHandled"));
+ checkState(args.length == 1);
+ checkState(args[0] instanceof Method);
+ Method invokedMethod = (Method) args[0];
+ String methodName = invokedMethod.getName();
+ Class<?>[] params = invokedMethod.getParameterTypes();
+ if ("equals".equals(methodName) && params.length == 1
+ && Object.class.equals(params[0])) {
+ return false;
+ }
+ if ("hashCode".equals(methodName) && params.length == 0) {
+ return false;
+ }
+ if ("toString".equals(methodName) && params.length == 0) {
+ return false;
+ }
+ if ("finalize".equals(methodName) && params.length == 0) {
+ return false;
+ }
+ return true;
+ }
+ };
+ Object methodFilter = Proxy.newProxyInstance(getClassLoader(),
+ new Class<?>[] { methodFilterClass }, methodFilterHandler);
+ Method setFilterMethod = proxyFactoryClass.getMethod("setFilter", methodFilterClass);
+ setFilterMethod.invoke(proxyFactory, methodFilter);
+ Method createClassMethod = proxyFactoryClass.getMethod("createClass");
+ Class<?> createdClass = (Class<?>) createClassMethod.invoke(proxyFactory);
+ InvocationHandler methodHandlerHandler = new InvocationHandler() {
+ @SuppressWarnings("unused")
+ public InvocationHandler $$getOriginal() {
+ return handler;
+ }
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ checkState(method.getName().equals("invoke"));
+ checkState(args.length == 4);
+ checkState(args[1] instanceof Method);
+ Method invokedMethod = (Method) args[1];
+ checkState(args[3] instanceof Object[]);
+ return handler.invoke(args[0], invokedMethod, (Object[]) args[3]);
+ }
+ };
+ Class<?> methodHandlerClass = Class.forName("javassist.util.proxy.MethodHandler");
+ Object methodHandler = Proxy.newProxyInstance(getClassLoader(),
+ new Class<?>[] { methodHandlerClass }, methodHandlerHandler);
+ Object proxy = unsafeCreateInstance(createdClass);
+ Class<?> proxyObjectClass = Class.forName("javassist.util.proxy.ProxyObject");
+ Method setHandlerMethod = proxyObjectClass.getMethod("setHandler", methodHandlerClass);
+ setHandlerMethod.invoke(proxy, methodHandler);
+ return proxy;
+ } catch (Exception e) {
+ // Not supported, something went wrong. Fall through, try android dexmaker.
+ e.printStackTrace(System.err);
+ }
+ }
+ // So, this is a class. First try using Android's ProxyBuilder from dexmaker.
try {
Class<?> proxyBuilder = Class.forName("com.google.dexmaker.stock.ProxyBuilder");
Method forClassMethod = proxyBuilder.getMethod("forClass", Class.class);
@@ -1122,6 +1287,15 @@ public class LittleMock {
/** Attempt to construct an instance of the class using hacky methods to avoid calling super. */
@SuppressWarnings("unchecked")
private static <T> T unsafeCreateInstance(Class<T> clazz) {
+ // try jvm
+ try {
+ Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
+ Field f = unsafeClass.getDeclaredField("theUnsafe");
+ f.setAccessible(true);
+ final Object unsafe = f.get(null);
+ final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
+ return (T) allocateInstance.invoke(unsafe, clazz);
+ } catch (Exception ignored) {}
// try dalvikvm, pre-gingerbread
try {
final Method newInstance = ObjectInputStream.class.getDeclaredMethod(
@@ -1142,4 +1316,31 @@ public class LittleMock {
} catch (Exception ignored) {}
throw new IllegalStateException("unsafe create instance failed");
}
+
+ /** See {@link LittleMock#inOrder(Object[])}. */
+ public interface InOrder {
+ <T> T verify(T mock);
+ }
+
+ /**
+ * Used to verify that invocations happen in a given order.
+ * <p>
+ * Still slight experimental at the moment: you can only verify one method call at a time,
+ * and we ignore the mocks array you pass in as an argument, you may use the returned inorder
+ * to verify all mocks.
+ * <p>
+ * This implementation is simple: the InOrder you get from this call can be used to verify that
+ * a sequence of method calls happened in the order you specify. Every verify you make with
+ * this InOrder will be compared with every other verify you made, to make sure that all the
+ * original invocations happened in exactly that same order.
+ */
+ public static InOrder inOrder(Object... mocks) {
+ return new InOrder() {
+ private final OrderChecker mChecker = new OrderChecker();
+ @Override
+ public <T> T verify(T mock) {
+ return LittleMock.verify(mock, times(1), mChecker);
+ }
+ };
+ }
}