diff options
-rw-r--r-- | src/com/google/testing/littlemock/LittleMock.java | 235 | ||||
-rw-r--r-- | tests/com/google/testing/littlemock/LittleMockTest.java | 157 |
2 files changed, 355 insertions, 37 deletions
diff --git a/src/com/google/testing/littlemock/LittleMock.java b/src/com/google/testing/littlemock/LittleMock.java index fc6c527..9d756dc 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); } @@ -422,6 +457,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); @@ -450,12 +486,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(). */ @@ -465,6 +506,7 @@ public class LittleMock { mMethod = method; mElement = element; mArgs = args; + mInvocationOrder = sGlobalInvocationOrder.getAndIncrement(); } public boolean argsMatch(Object[] args) { @@ -477,6 +519,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. * @@ -523,6 +579,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. @@ -591,11 +648,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; } } }); @@ -639,9 +697,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); } } @@ -650,24 +708,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); @@ -744,44 +803,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, @@ -884,7 +946,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); } @@ -966,6 +1028,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) { @@ -1031,6 +1099,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}. @@ -1050,14 +1125,92 @@ 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); } /** 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. + 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, i.e. javassist missing. Fall through. + } + // 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); @@ -1080,6 +1233,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( @@ -1100,4 +1262,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); + } + }; + } } diff --git a/tests/com/google/testing/littlemock/LittleMockTest.java b/tests/com/google/testing/littlemock/LittleMockTest.java index fde8c7c..e7f9bca 100644 --- a/tests/com/google/testing/littlemock/LittleMockTest.java +++ b/tests/com/google/testing/littlemock/LittleMockTest.java @@ -37,8 +37,10 @@ import static com.google.testing.littlemock.LittleMock.doNothing; import static com.google.testing.littlemock.LittleMock.doReturn; import static com.google.testing.littlemock.LittleMock.doThrow; import static com.google.testing.littlemock.LittleMock.eq; +import static com.google.testing.littlemock.LittleMock.inOrder; import static com.google.testing.littlemock.LittleMock.initMocks; import static com.google.testing.littlemock.LittleMock.isA; +import static com.google.testing.littlemock.LittleMock.matches; import static com.google.testing.littlemock.LittleMock.mock; import static com.google.testing.littlemock.LittleMock.never; import static com.google.testing.littlemock.LittleMock.reset; @@ -48,6 +50,9 @@ import static com.google.testing.littlemock.LittleMock.verify; import static com.google.testing.littlemock.LittleMock.verifyNoMoreInteractions; import static com.google.testing.littlemock.LittleMock.verifyZeroInteractions; +import com.google.testing.littlemock.LittleMock.ArgumentMatcher; +import com.google.testing.littlemock.LittleMock.InOrder; + import junit.framework.TestCase; import java.io.IOException; @@ -1413,16 +1418,151 @@ public class LittleMockTest extends TestCase { } catch (IllegalStateException expected) {} } + public void testCustomMatcher() { + ArgumentMatcher argumentMatcher = new ArgumentMatcher() { + @Override + public boolean matches(Object value) { + return ((String) value).contains("[]"); + } + }; + mFoo.add("as[]df"); + mFoo.add("qwer[]asdf"); + mFoo.add("1234"); + verify(mFoo, times(3)).add(anyString()); + verify(mFoo, times(2)).add((String) matches(argumentMatcher)); + } + + public void testInorderExample_Success() { + @SuppressWarnings("unchecked") + List<String> firstMock = mock(List.class); + @SuppressWarnings("unchecked") + List<String> secondMock = mock(List.class); + firstMock.add("was called first"); + secondMock.add("was called second"); + InOrder inOrder = inOrder(firstMock, secondMock); + inOrder.verify(firstMock).add("was called first"); + inOrder.verify(secondMock).add("was called second"); + } + + public void testInorderExample_Failure() { + @SuppressWarnings("unchecked") + List<String> firstMock = mock(List.class); + @SuppressWarnings("unchecked") + List<String> secondMock = mock(List.class); + firstMock.add("was called first"); + secondMock.add("was called second"); + InOrder inOrder = inOrder(firstMock, secondMock); + inOrder.verify(secondMock).add("was called second"); + try { + inOrder.verify(firstMock).add("was called first"); + throw new IllegalStateException(); + } catch (AssertionError expected) {} + } + + public void testInorderInterleave() { + @SuppressWarnings("unchecked") + List<String> firstMock = mock(List.class); + firstMock.add("a"); + firstMock.add("b"); + firstMock.add("a"); + + // Should be fine to verify a then b, since they happened in that order. + InOrder inOrder = inOrder(firstMock); + inOrder.verify(firstMock).add("a"); + inOrder.verify(firstMock).add("b"); + + // Should also be fine to inorder verify the other way around, they happened in that order too. + inOrder = inOrder(firstMock); + inOrder.verify(firstMock).add("b"); + inOrder.verify(firstMock).add("a"); + + // Should be fine to verify "a, b, a" since that too happened. + inOrder = inOrder(firstMock); + inOrder.verify(firstMock).add("a"); + inOrder.verify(firstMock).add("b"); + inOrder.verify(firstMock).add("a"); + + // "a, a, b" did not happen. + inOrder = inOrder(firstMock); + inOrder.verify(firstMock).add("a"); + inOrder.verify(firstMock).add("a"); + try { + inOrder.verify(firstMock).add("b"); + throw new IllegalStateException(); + } catch (AssertionError expected) {} + + // "b, a, b" did not happen. + inOrder = inOrder(firstMock); + inOrder.verify(firstMock).add("b"); + inOrder.verify(firstMock).add("a"); + try { + inOrder.verify(firstMock).add("b"); + throw new IllegalStateException(); + } catch (AssertionError expected) {} + + // "b" did not happen twice. + inOrder = inOrder(firstMock); + inOrder.verify(firstMock).add("b"); + try { + inOrder.verify(firstMock).add("b"); + throw new IllegalStateException(); + } catch (AssertionError expected) {} + } + + public void testInorderComplicatedExample() { + // TODO: I'm currently totally ignoring the parameters passed to the inorder method. + // I don't understand what the point of them is, anyway. + @SuppressWarnings("unchecked") + List<String> firstMock = mock(List.class); + @SuppressWarnings("unchecked") + List<String> secondMock = mock(List.class); + + firstMock.add("1"); + secondMock.add("2"); + firstMock.add("3"); + secondMock.add("4"); + + InOrder allInOrder = inOrder(firstMock, secondMock); + allInOrder.verify(firstMock).add("1"); + allInOrder.verify(secondMock).add("2"); + allInOrder.verify(firstMock).add("3"); + allInOrder.verify(secondMock).add("4"); + + InOrder firstInOrder = inOrder(firstMock, secondMock); + firstInOrder.verify(firstMock).add("1"); + firstInOrder.verify(firstMock).add("3"); + try { + firstInOrder.verify(secondMock).add("2"); + throw new IllegalStateException(); + } catch (AssertionError expected) {} + firstInOrder.verify(secondMock).add("4"); + + InOrder secondInOrder = inOrder(firstMock, secondMock); + secondInOrder.verify(secondMock).add("2"); + secondInOrder.verify(secondMock).add("4"); + try { + secondInOrder.verify(firstMock).add("1"); + throw new IllegalStateException(); + } catch (AssertionError expected) {} + try { + secondInOrder.verify(firstMock).add("3"); + throw new IllegalStateException(); + } catch (AssertionError expected) {} + } + public static class Jim { - public void bob() { + public int bob() { fail(); + return 3; } } // Does not work on JVM, android only. - public void suppress_testMockingConcreteClasses() throws Exception { + public void testMockingConcreteClasses() throws Exception { Jim mock = mock(Jim.class); - mock.bob(); + assertEquals(0, mock.bob()); + doReturn(8).when(mock).bob(); + assertEquals(8, mock.bob()); } private Future<Void> invokeBarMethodAfterLatchAwait(final CountDownLatch countDownLatch) { @@ -1439,17 +1579,6 @@ public class LittleMockTest extends TestCase { // TODO(hugohudson): 5. Every method that throws exceptions could be improved by adding // test for the content of the error message. - // TODO(hugohudson): 5. Add InOrder class, so that we can check that the given methods on - // the given mocks happen in the right order. It will be pretty easy to do. The syntax - // looks like this: - // InOrder inOrder = inOrder(firstMock, secondMock); - // inOrder.verify(firstMock).firstMethod(); - // inOrder.verify(secondMock).secondMethod(); - // This allows us to verify that the calls happened in the desired order. - // By far the simplest way to do this is have a static AtomicInteger on the class which - // indicates exactly when every method call happened, and then just compare order based on - // that. - // TODO(hugohudson): 5. Make the doReturn() method take variable arguments. // The syntax is: // doReturn(1, 2, 3).when(mFoo).anInt(); |