diff options
Diffstat (limited to 'src/com/google/testing/littlemock/LittleMock.java')
-rw-r--r-- | src/com/google/testing/littlemock/LittleMock.java | 247 |
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); + } + }; + } } |