diff options
author | Hugo Hudson <hugohudson@google.com> | 2012-01-10 17:34:02 +0000 |
---|---|---|
committer | Hugo Hudson <hugohudson@google.com> | 2012-01-10 17:35:38 +0000 |
commit | ebb9a5e46fa2c58fa091a8de2c12d87828fae2b0 (patch) | |
tree | 2e4aed3ad2a179efddf4ca2bf7f73c9e79f488b1 | |
parent | da0be1ebc3a6f7dbd434a3a60c5b89a1f96cbc62 (diff) | |
download | littlemock-ebb9a5e46fa2c58fa091a8de2c12d87828fae2b0.tar.gz |
Updates to r6 of littlemock, concrete classes.
- Updates source to r6.
Highlights:
- Can now mock concrete classes if you have dexmaker.jar on your
classpath.
- Can use blocking verify() calls with timeout() method.
- Thread-safe mocks, and only one thread can verify() and stub().
Change-Id: I42f55cb33d2ed99097317705a35e73e63f8a864f
-rw-r--r-- | README | 4 | ||||
-rw-r--r-- | src/com/google/testing/littlemock/AppDataDirGuesser.java | 97 | ||||
-rw-r--r-- | src/com/google/testing/littlemock/LittleMock.java | 272 | ||||
-rw-r--r-- | tests/com/google/testing/littlemock/AppDataDirGuesserTest.java | 101 | ||||
-rw-r--r-- | tests/com/google/testing/littlemock/LittleMockTest.java | 596 |
5 files changed, 848 insertions, 222 deletions
@@ -8,8 +8,8 @@ License: Apache 2.0 Description: Mocking framework. Looks very like Mockito (see http://mockito.org). -Has a number of restrictions - mocks interfaces only, no class -generation. Has no dependencies, consequently can be used on Android. +Has a number of restrictions. +Can mock concrete classes, requires dexmaker.jar on classpath. Local Modifications: No modifications. diff --git a/src/com/google/testing/littlemock/AppDataDirGuesser.java b/src/com/google/testing/littlemock/AppDataDirGuesser.java new file mode 100644 index 0000000..0fdd475 --- /dev/null +++ b/src/com/google/testing/littlemock/AppDataDirGuesser.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.littlemock; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class for helping guess the application data directory. + */ +public class AppDataDirGuesser { + + /** A single default instance of app data dir guesser, for overriding if you really need to. */ + private static volatile AppDataDirGuesser sInjectableInstance = new AppDataDirGuesser(); + + public static void setInstance(AppDataDirGuesser instance) { + sInjectableInstance = instance; + } + + public static AppDataDirGuesser getsInstance() { + return sInjectableInstance; + } + + public File guessSuitableDirectoryForGeneratedClasses() { + try { + ClassLoader classLoader = AppDataDirGuesser.class.getClassLoader(); + // Check that we have an instance of the PathClassLoader. + Class<?> clazz = Class.forName("dalvik.system.PathClassLoader"); + clazz.cast(classLoader); + // Use the toString() method to calculate the data directory. + String pathFromThisClassLoader = getPathFromThisClassLoader(classLoader); + File[] results = guessPath(pathFromThisClassLoader); + if (results.length > 0) { + return results[0]; + } + } catch (ClassCastException e) { + // Fall through, we will return null. + } catch (ClassNotFoundException e) { + // Fall through, we will return null. + } + return null; + } + + private String getPathFromThisClassLoader(ClassLoader classLoader) { + // Parsing toString() method: yuck. But no other way to get the path. + // Strip out the bit between angle brackets, that's our path. + String result = classLoader.toString(); + int index = result.lastIndexOf('['); + result = (index == -1) ? result : result.substring(index + 1); + index = result.indexOf(']'); + return (index == -1) ? result : result.substring(0, index); + } + + // @VisibleForTesting + File[] guessPath(String input) { + List<File> results = new ArrayList<File>(); + for (String potential : input.split(":")) { + if (!potential.startsWith("/data/app/")) { + continue; + } + int start = "/data/app/".length(); + int end = potential.lastIndexOf(".apk"); + if (end != potential.length() - 4) { + continue; + } + int dash = potential.indexOf("-"); + if (dash != -1) { + end = dash; + } + File file = new File("/data/data/" + potential.substring(start, end) + "/cache"); + if (isWriteableDirectory(file)) { + results.add(file); + } + } + return results.toArray(new File[results.size()]); + } + + // @VisibleForTesting + boolean isWriteableDirectory(File file) { + return file.isDirectory() && file.canWrite(); + } +} diff --git a/src/com/google/testing/littlemock/LittleMock.java b/src/com/google/testing/littlemock/LittleMock.java index 03e2c7a..c8f2b2f 100644 --- a/src/com/google/testing/littlemock/LittleMock.java +++ b/src/com/google/testing/littlemock/LittleMock.java @@ -16,6 +16,7 @@ package com.google.testing.littlemock; +import java.io.File; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; @@ -23,10 +24,11 @@ import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicReference; /** * Very lightweight and simple mocking framework, inspired by Mockito, http://mockito.org. @@ -35,9 +37,6 @@ import java.util.concurrent.Callable; * <ul> * <li>Doesn't support mocking concrete classes, <b>interfaces only</b>.</li> * - * <li><b>Is not thread-safe.</b> You'll need to provide your own synchronization if you want to - * run multi-threaded unit tests.</li> - * * <li>It supports only a <b>small subset</b> of the APIs provided by Mockito and other mocking * frameworks.</li> * </ul> @@ -149,11 +148,21 @@ public class LittleMock { 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.mStubbingAction == null, "Unfinished stubbing statements"); checkNoMatchers(); - getHandlerFrom(mock).mHowManyTimes = howManyTimes; - return mock; + handler.mHowManyTimes = howManyTimes; + sUnfinishedCallCounts.add(howManyTimes); + return handler.<T>getVerifyingMock(); } + /** The list of outstanding calls to verify() that haven't finished, used to check for errors. */ + private static List<CallCount> sUnfinishedCallCounts = new ArrayList<CallCount>(); + + /** The list of outstanding calls to when() that haven't finished, used to check for errors. */ + private static List<Action> sUnfinishedStubbingActions = new ArrayList<Action>(); + /** Begins a verification step for exactly one method call. */ public static <T> T verify(T mock) { return verify(mock, times(1)); } @@ -161,7 +170,7 @@ public class LittleMock { public static void verifyZeroInteractions(Object... mocks) { checkNoMatchers(); for (Object mock : mocks) { - LinkedList<MethodCall> mMethodCalls = getHandlerFrom(mock).mRecordedCalls; + List<MethodCall> mMethodCalls = getHandlerFrom(mock).mRecordedCalls; expect(mMethodCalls.isEmpty(), "Mock expected zero interactions, had " + mMethodCalls); } } @@ -200,7 +209,12 @@ public class LittleMock { /** Creates a {@link CallCount} that matches exactly the given number of calls. */ public static CallCount times(long n) { return new CallCount(n, n); } - /** Creates a {@link CallCount} that only matches if the method was never called. */ + /** Claims that the verified call must happen before the given timeout. */ + public static Timeout timeout(long timeoutMillis) { + return new Timeout(1, 1, timeoutMillis); + } + +/** Creates a {@link CallCount} that only matches if the method was never called. */ public static CallCount never() { return new CallCount(0, 0); } /** Creates a {@link CallCount} that matches at least one method call. */ @@ -362,8 +376,9 @@ public class LittleMock { } /** Creates a mock, more easily done via the {@link #initMocks(Object)} method. */ + @SuppressWarnings("unchecked") private static <T> T mock(Class<T> clazz, String fieldName) { - return LittleMock.<T>newProxy(clazz, new DefaultInvocationHandler(clazz, fieldName)); + return (T) createProxy(clazz, new DefaultInvocationHandler(clazz, fieldName)); } /** Pick a suitable name for a field of the given clazz. */ @@ -380,7 +395,19 @@ public class LittleMock { } /** Use this in tear down to check for programming errors. */ - public static void checkForProgrammingErrorsDuringTearDown() { checkNoMatchers(); } + public static void checkForProgrammingErrorsDuringTearDown() { + checkNoMatchers(); + checkNoUnfinishedCalls(sUnfinishedCallCounts, "verify()"); + checkNoUnfinishedCalls(sUnfinishedStubbingActions, "stubbing"); + } + + /** Helper function to check that there are no verify or stubbing commands outstanding. */ + private static void checkNoUnfinishedCalls(List<?> list, String type) { + if (!list.isEmpty()) { + list.clear(); + throw new IllegalStateException("Unfinished " + type + " statements"); + } + } /** Implementation of {@link Behaviour}. */ private static class BehaviourImpl implements Behaviour { @@ -392,8 +419,12 @@ public class LittleMock { @Override public <T> T when(T mock) { - getHandlerFrom(mock).mStubbingAction = mAction; - return mock; + DefaultInvocationHandler handler = getHandlerFrom(mock); + checkState(handler.mHowManyTimes == null, "Unfinished verify() statements"); + checkState(handler.mStubbingAction == null, "Unfinished stubbing statements"); + handler.mStubbingAction = mAction; + sUnfinishedStubbingActions.add(mAction); + return handler.<T>getStubbingMock(); } } @@ -478,9 +509,10 @@ public class LittleMock { private final String mFieldName; /** The list of method calls executed on the mock. */ - private LinkedList<MethodCall> mRecordedCalls = new LinkedList<MethodCall>(); + private List<MethodCall> mRecordedCalls = new CopyOnWriteArrayList<MethodCall>(); /** The list of method calls that were stubbed out and their corresponding actions. */ - private LinkedList<StubbedCall> mStubbedCalls = new LinkedList<StubbedCall>(); + private List<StubbedCall> mStubbedCalls = new CopyOnWriteArrayList<StubbedCall>(); + /** * The number of times a given call should be verified. * @@ -490,6 +522,7 @@ public class LittleMock { * <p>It is reset to null once the verification has occurred. */ private CallCount mHowManyTimes = null; + /** * The action to be associated with the stubbed method. * @@ -498,6 +531,12 @@ public class LittleMock { */ private Action mStubbingAction = null; + /** Dynamic proxy used to verify calls against this mock. */ + private final Object mVerifyingMock; + + /** Dynamic proxy used to stub calls against this mock. */ + private final Object mStubbingMock; + /** * Creates a new invocation handler for an object. * @@ -509,24 +548,84 @@ public class LittleMock { public DefaultInvocationHandler(Class<?> clazz, String fieldName) { mClazz = clazz; mFieldName = fieldName; + mVerifyingMock = createVerifyingMock(clazz); + mStubbingMock = createStubbingMock(clazz); + } + + // Safe if you call getHandlerFrom(mock).getVerifyingMock(), since this is guaranteed to be + // of the same type as mock itself. + @SuppressWarnings("unchecked") + public <T> T getVerifyingMock() { + return (T) mVerifyingMock; + } + + // Safe if you call getHandlerFrom(mock).getStubbingMock(), since this is guaranteed to be + // of the same type as mock itself. + @SuppressWarnings("unchecked") + public <T> T getStubbingMock() { + return (T) mStubbingMock; + } + + /** Used to check that we always stub and verify from the same thread. */ + private AtomicReference<Thread> mCurrentThread = new AtomicReference<Thread>(); + + /** Check that we are stubbing and verifying always from the same thread. */ + private void checkThread() { + Thread currentThread = Thread.currentThread(); + mCurrentThread.compareAndSet(null, currentThread); + if (mCurrentThread.get() != currentThread) { + throw new IllegalStateException("Must always mock and stub from one thread only. " + + "This thread: " + currentThread + ", the other thread: " + mCurrentThread.get()); + } + } + + /** Generate the dynamic proxy that will handle verify invocations. */ + private Object createVerifyingMock(Class<?> clazz) { + return createProxy(clazz, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + checkThread(); + expect(mHowManyTimes != null, "verifying mock doesn't know how many times"); + try { + ArgumentMatcher[] matchers = checkClearAndGetMatchers(method); + StackTraceElement callSite = new Exception().getStackTrace()[2]; + MethodCall methodCall = new MethodCall(method, callSite, args); + innerVerify(method, matchers, methodCall, proxy, callSite, mHowManyTimes); + return defaultReturnValue(method.getReturnType()); + } finally { + sUnfinishedCallCounts.remove(mHowManyTimes); + mHowManyTimes = null; + } + } + }); + } + + /** Generate the dynamic proxy that will handle stubbing invocations. */ + private Object createStubbingMock(Class<?> clazz) { + return createProxy(clazz, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + checkThread(); + expect(mStubbingAction != null, "stubbing mock doesn't know what action to perform"); + try { + ArgumentMatcher[] matchers = checkClearAndGetMatchers(method); + StackTraceElement callSite = new Exception().getStackTrace()[2]; + MethodCall methodCall = new MethodCall(method, callSite, args); + innerStub(method, matchers, methodCall, callSite, mStubbingAction); + return defaultReturnValue(method.getReturnType()); + } finally { + sUnfinishedStubbingActions.remove(mStubbingAction); + mStubbingAction = null; + } + } + }); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - ArgumentMatcher[] matchers = checkClearAndGetMatchers(method); StackTraceElement callSite = new Exception().getStackTrace()[2]; MethodCall methodCall = new MethodCall(method, callSite, args); - if (mHowManyTimes != null) { - innerVerify(method, matchers, methodCall, proxy, callSite); - mHowManyTimes = null; - return defaultReturnValue(method.getReturnType()); - } else if (mStubbingAction != null) { - innerStub(method, matchers, methodCall, callSite); - mStubbingAction = null; - return defaultReturnValue(method.getReturnType()); - } else { - return innerRecord(method, args, methodCall, proxy, callSite); - } + return innerRecord(method, args, methodCall, proxy, callSite); } /** @@ -579,13 +678,12 @@ public class LittleMock { } private void innerStub(Method method, final ArgumentMatcher[] matchers, MethodCall methodCall, - StackTraceElement callSite) { - final Action stubbingAction = mStubbingAction; + StackTraceElement callSite, final Action stubbingAction) { checkSpecialObjectMethods(method, "stub"); checkThisActionCanBeUsedForThisMethod(method, stubbingAction, callSite); if (matchers.length == 0) { // If there are no matchers, then this is a simple stubbed method call with an action. - mStubbedCalls.addFirst(new StubbedCall(methodCall, stubbingAction)); + mStubbedCalls.add(0, new StubbedCall(methodCall, stubbingAction)); return; } // If there are matchers, then we need to make a new method call which matches only @@ -607,7 +705,7 @@ public class LittleMock { return stubbingAction.getReturnType(); } }; - mStubbedCalls.addFirst(new StubbedCall(matchMatchersMethodCall, setCapturesThenAction)); + mStubbedCalls.add(0, new StubbedCall(matchMatchersMethodCall, setCapturesThenAction)); } private void checkThisActionCanBeUsedForThisMethod(Method method, final Action stubbingAction, @@ -645,8 +743,32 @@ public class LittleMock { } private void innerVerify(Method method, ArgumentMatcher[] matchers, MethodCall methodCall, - Object proxy, StackTraceElement callSite) { + Object proxy, StackTraceElement callSite, CallCount callCount) { checkSpecialObjectMethods(method, "verify"); + int total = countMatchingInvocations(method, matchers, methodCall); + long callTimeout = callCount.getTimeout(); + if (callTimeout > 0) { + long endTime = System.currentTimeMillis() + callTimeout; + while (!callCount.matches(total)) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + fail("interrupted whilst waiting to verify"); + } + if (System.currentTimeMillis() > endTime) { + fail(formatFailedVerifyMessage(methodCall, total, callTimeout, callCount)); + } + total = countMatchingInvocations(method, matchers, methodCall); + } + } else { + if (!callCount.matches(total)) { + fail(formatFailedVerifyMessage(methodCall, total, 0, callCount)); + } + } + } + + private int countMatchingInvocations(Method method, ArgumentMatcher[] matchers, + MethodCall methodCall) { int total = 0; for (MethodCall call : mRecordedCalls) { if (call.mMethod.equals(method)) { @@ -658,17 +780,22 @@ public class LittleMock { } } } - expect(mHowManyTimes.matches(total), formatFailedVerifyMessage(methodCall, total)); + return total; } - private String formatFailedVerifyMessage(MethodCall methodCall, int total) { + private String formatFailedVerifyMessage(MethodCall methodCall, int total, long timeoutMillis, + CallCount callCount) { StringBuffer sb = new StringBuffer(); - sb.append("\nExpected ").append(mHowManyTimes).append(" to:"); + sb.append("\nExpected ").append(callCount); + if (timeoutMillis > 0) { + sb.append(" within " + timeoutMillis + "ms"); + } + sb.append(" to:"); appendDebugStringForMethodCall(sb, methodCall.mMethod, methodCall.mElement, mFieldName, false); sb.append("\n\n"); if (mRecordedCalls.size() == 0) { - sb.append("No method calls happened to this mock"); + sb.append("No method calls happened on this mock"); } else { sb.append("Method calls that did happen:"); for (MethodCall recordedCall : mRecordedCalls) { @@ -768,10 +895,10 @@ public class LittleMock { /** Encapsulates the number of times a method is called, between upper and lower bounds. */ private static class CallCount { - long mLowerBound; - long mUpperBound; + private long mLowerBound; + private long mUpperBound; - private CallCount(long lowerBound, long upperBound) { + public CallCount(long lowerBound, long upperBound) { mLowerBound = lowerBound; mUpperBound = upperBound; } @@ -781,6 +908,21 @@ public class LittleMock { return total >= mLowerBound && total <= mUpperBound; } + /** Tells us how long we should block waiting for the verify to happen. */ + public long getTimeout() { + return 0; + } + + public CallCount setLowerBound(long lowerBound) { + mLowerBound = lowerBound; + return this; + } + + public CallCount setUpperBound(long upperBound) { + mUpperBound = upperBound; + return this; + } + @Override public String toString() { if (mLowerBound == mUpperBound) { @@ -792,6 +934,26 @@ public class LittleMock { } } + /** Encapsulates adding number of times behaviour to a call count with timeout. */ + public static final class Timeout extends CallCount { + private long mTimeoutMillis; + + public Timeout(long lowerBound, long upperBound, long timeoutMillis) { + super(lowerBound, upperBound); + mTimeoutMillis = timeoutMillis; + } + + @Override + public long getTimeout() { + return mTimeoutMillis; + } + + public CallCount times(int times) { return setLowerBound(times).setUpperBound(times); } + public CallCount atLeast(long n) { return setLowerBound(n).setUpperBound(Long.MAX_VALUE); } + public CallCount atLeastOnce() { return setLowerBound(1).setUpperBound(Long.MAX_VALUE); } + public CallCount between(long n, long m) { return setLowerBound(n).setUpperBound(m); } + } + /** Helper method to add an 's' to a string iff the count is not 1. */ private static String plural(String prefix, long count) { return (count == 1) ? prefix : (prefix + "s"); @@ -861,12 +1023,6 @@ public class LittleMock { return (DefaultInvocationHandler) invocationHandler; } - /** Builds a dynamic proxy that implements the given interface, delegating to given handler. */ - @SuppressWarnings("unchecked") - private static <T> T newProxy(Class<?> theInterface, InvocationHandler theHandler) { - return (T) Proxy.newProxyInstance(getClassLoader(), new Class<?>[]{ theInterface }, theHandler); - } - /** Gets a suitable class loader for use with the proxy. */ private static ClassLoader getClassLoader() { return LittleMock.class.getClassLoader(); @@ -878,4 +1034,32 @@ public class LittleMock { field.set(object, value); field.setAccessible(false); } + + /** Helper method to throw an IllegalStateException if given condition is not met. */ + private static void checkState(boolean condition, String message) { + if (!condition) { + throw new IllegalStateException(message); + } + } + + /** Create a dynamic proxy for the given class, delegating to the given invocation handler. */ + private static Object createProxy(Class<?> clazz, InvocationHandler handler) { + if (clazz.isInterface()) { + return Proxy.newProxyInstance(getClassLoader(), new Class<?>[] { clazz }, handler); + } + try { + Class<?> proxyBuilder = Class.forName("com.google.dexmaker.stock.ProxyBuilder"); + Method forClassMethod = proxyBuilder.getMethod("forClass", Class.class); + Object builder = forClassMethod.invoke(null, clazz); + Method handlerMethod = builder.getClass().getMethod("handler", InvocationHandler.class); + builder = handlerMethod.invoke(builder, handler); + Method dexCacheMethod = builder.getClass().getMethod("dexCache", File.class); + File directory = AppDataDirGuesser.getsInstance().guessSuitableDirectoryForGeneratedClasses(); + builder = dexCacheMethod.invoke(builder, directory); + Method buildMethod = builder.getClass().getMethod("build"); + return buildMethod.invoke(builder); + } catch (Exception e) { + throw new IllegalStateException("Could not mock this concrete class", e); + } + } } diff --git a/tests/com/google/testing/littlemock/AppDataDirGuesserTest.java b/tests/com/google/testing/littlemock/AppDataDirGuesserTest.java new file mode 100644 index 0000000..b537097 --- /dev/null +++ b/tests/com/google/testing/littlemock/AppDataDirGuesserTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.littlemock; + +import junit.framework.TestCase; + +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class AppDataDirGuesserTest extends TestCase { + public void testGuessCacheDir_SimpleExample() { + guessCacheDirFor("/data/app/a.b.c.apk").shouldGive("/data/data/a.b.c/cache"); + guessCacheDirFor("/data/app/a.b.c.tests.apk").shouldGive("/data/data/a.b.c.tests/cache"); + } + + public void testGuessCacheDir_MultipleResultsSeparatedByColon() { + guessCacheDirFor("/data/app/a.b.c.apk:/data/app/d.e.f.apk") + .shouldGive("/data/data/a.b.c/cache", "/data/data/d.e.f/cache"); + } + + public void testGuessCacheDir_NotWriteableSkipped() { + guessCacheDirFor("/data/app/a.b.c.apk:/data/app/d.e.f.apk") + .withNonWriteable("/data/data/a.b.c/cache") + .shouldGive("/data/data/d.e.f/cache"); + } + + public void testGuessCacheDir_StripHyphenatedSuffixes() { + guessCacheDirFor("/data/app/a.b.c-2.apk").shouldGive("/data/data/a.b.c/cache"); + } + + public void testGuessCacheDir_LeadingAndTrailingColonsIgnored() { + guessCacheDirFor("/data/app/a.b.c.apk:asdf:").shouldGive("/data/data/a.b.c/cache"); + guessCacheDirFor(":asdf:/data/app/a.b.c.apk").shouldGive("/data/data/a.b.c/cache"); + } + + public void testGuessCacheDir_InvalidInputsGiveEmptyArray() { + guessCacheDirFor("").shouldGive(); + } + + public void testGuessCacheDir_JarsIgnored() { + guessCacheDirFor("/data/app/a.b.c.jar").shouldGive(); + guessCacheDirFor("/system/framework/android.test.runner.jar").shouldGive(); + } + + public void testGuessCacheDir_RealWorldExample() { + String realPath = "/system/framework/android.test.runner.jar:" + + "/data/app/com.google.android.voicesearch.tests-2.apk:" + + "/data/app/com.google.android.voicesearch-1.apk"; + guessCacheDirFor(realPath) + .withNonWriteable("/data/data/com.google.android.voicesearch.tests/cache") + .shouldGive("/data/data/com.google.android.voicesearch/cache"); + } + + private interface TestCondition { + TestCondition withNonWriteable(String... files); + void shouldGive(String... files); + } + + private TestCondition guessCacheDirFor(final String path) { + final Set<String> notWriteable = new HashSet<String>(); + return new TestCondition() { + @Override + public void shouldGive(String... files) { + AppDataDirGuesser guesser = new AppDataDirGuesser() { + @Override + public boolean isWriteableDirectory(File file) { + return !notWriteable.contains(file.getAbsolutePath()); + } + }; + File[] results = guesser.guessPath(path); + assertNotNull("Null results for " + path, results); + assertEquals("Bad lengths for " + path, files.length, results.length); + for (int i = 0; i < files.length; ++i) { + assertEquals("Element " + i, new File(files[i]), results[i]); + } + } + + @Override + public TestCondition withNonWriteable(String... files) { + notWriteable.addAll(Arrays.asList(files)); + return this; + } + }; + } +} diff --git a/tests/com/google/testing/littlemock/LittleMockTest.java b/tests/com/google/testing/littlemock/LittleMockTest.java index 9d3cf38..e8c7b1e 100644 --- a/tests/com/google/testing/littlemock/LittleMockTest.java +++ b/tests/com/google/testing/littlemock/LittleMockTest.java @@ -42,6 +42,7 @@ import static com.google.testing.littlemock.LittleMock.isA; import static com.google.testing.littlemock.LittleMock.mock; import static com.google.testing.littlemock.LittleMock.never; import static com.google.testing.littlemock.LittleMock.reset; +import static com.google.testing.littlemock.LittleMock.timeout; import static com.google.testing.littlemock.LittleMock.times; import static com.google.testing.littlemock.LittleMock.verify; import static com.google.testing.littlemock.LittleMock.verifyNoMoreInteractions; @@ -50,16 +51,18 @@ import static com.google.testing.littlemock.LittleMock.verifyZeroInteractions; import junit.framework.TestCase; import java.io.IOException; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; /** * Unit tests for the LittleMock class. @@ -67,16 +70,6 @@ import java.util.concurrent.Callable; * @author hugohudson@gmail.com (Hugo Hudson) */ public class LittleMockTest extends TestCase { - /** - * Used in these unit tests to indicate that the method should throw a given type of exception. - */ - @Target({ ElementType.METHOD }) - @Retention(RetentionPolicy.RUNTIME) - public @interface ShouldThrow { - public Class<? extends Throwable> value(); - public String[] messages() default {}; - } - @Mock private Foo mFoo; @Mock private Bar mBar; @Mock private BarSubtype mBarSubtype; @@ -84,36 +77,20 @@ public class LittleMockTest extends TestCase { @Captor private ArgumentCaptor<String> mCaptureAnotherString; @Captor private ArgumentCaptor<Integer> mCaptureInteger; @Captor private ArgumentCaptor<Callback> mCaptureCallback; + private ExecutorService mExecutorService; @Override protected void setUp() throws Exception { super.setUp(); LittleMock.initMocks(this); + mExecutorService = Executors.newCachedThreadPool(); } @Override - protected void runTest() throws Throwable { - Method method = getClass().getMethod(getName(), (Class[]) null); - ShouldThrow shouldThrowAnnotation = method.getAnnotation(ShouldThrow.class); - if (shouldThrowAnnotation != null) { - try { - super.runTest(); - fail("Should have thrown " + shouldThrowAnnotation.value()); - } catch (Throwable e) { - if (!e.getClass().equals(shouldThrowAnnotation.value())) { - fail("Should have thrown " + shouldThrowAnnotation.value() + " but threw " + e); - } - for (String requiredSubstring : shouldThrowAnnotation.messages()) { - if (!e.getMessage().contains(requiredSubstring)) { - fail("Error message didn't contain " + requiredSubstring + ", was " + e.getMessage()); - } - } - // Good, test passes. - } - } else { - super.runTest(); + protected void tearDown() throws Exception { + mExecutorService.shutdown(); + super.tearDown(); } - } /** Simple interface for testing against. */ public interface Callback { @@ -186,14 +163,18 @@ public class LittleMockTest extends TestCase { assertEquals(null, mFoo.anInterface()); } - @ShouldThrow(IllegalArgumentException.class) public void testVerify_FailsIfNotDoneOnAProxy() { - verify("hello").contains("something"); + try { + verify("hello").contains("something"); + fail(); + } catch (IllegalArgumentException expected) {} } - @ShouldThrow(IllegalArgumentException.class) public void testVerify_FailsIfNotCreatedByOurMockMethod() { - verify(createNotARealMock()).add("something"); + try { + verify(createNotARealMock()).add("something"); + fail(); + } catch (IllegalArgumentException expected) {} } public void testVerify_SuccessfulVerification() { @@ -215,22 +196,28 @@ public class LittleMockTest extends TestCase { verify(mFoo).add("something"); } - @ShouldThrow(AssertionError.class) public void testVerify_MeansOnlyOnceSoShouldFailIfCalledTwice() { mFoo.add("something"); mFoo.add("something"); - verify(mFoo).add("something"); + try { + verify(mFoo).add("something"); + fail(); + } catch (AssertionError expected) {} } - @ShouldThrow(AssertionError.class) public void testVerify_FailedVerification_CalledWithWrongArgument() { mFoo.add("something else"); - verify(mFoo).add("something"); + try { + verify(mFoo).add("something"); + fail(); + } catch (AssertionError expected) {} } - @ShouldThrow(AssertionError.class) public void testVerify_FailedVerification_WasNeverCalled() { - verify(mFoo).add("something"); + try { + verify(mFoo).add("something"); + fail(); + } catch (AssertionError expected) {} } public void testVerify_TimesTwice_Succeeds() { @@ -239,25 +226,31 @@ public class LittleMockTest extends TestCase { verify(mFoo, LittleMock.times(2)).add("jim"); } - @ShouldThrow(AssertionError.class) public void testVerify_TimesTwice_ButThreeTimesFails() { mFoo.add("jim"); mFoo.add("jim"); mFoo.add("jim"); - verify(mFoo, LittleMock.times(2)).add("jim"); + try { + verify(mFoo, LittleMock.times(2)).add("jim"); + fail(); + } catch (AssertionError expected) {} } - @ShouldThrow(AssertionError.class) public void testVerify_TimesTwice_ButOnceFails() { mFoo.add("jim"); - verify(mFoo, LittleMock.times(2)).add("jim"); + try { + verify(mFoo, LittleMock.times(2)).add("jim"); + fail(); + } catch (AssertionError expected) {} } - @ShouldThrow(AssertionError.class) public void testVerify_TimesTwice_DifferentStringsFails() { mFoo.add("jim"); mFoo.add("bob"); - verify(mFoo, LittleMock.times(2)).add("jim"); + try { + verify(mFoo, LittleMock.times(2)).add("jim"); + fail(); + } catch (AssertionError expected) {} } public void testVerify_TimesTwice_WorksWithAnyString() { @@ -266,18 +259,22 @@ public class LittleMockTest extends TestCase { verify(mFoo, LittleMock.times(2)).add(anyString()); } - @ShouldThrow(AssertionError.class) public void testVerify_TimesTwice_FailsIfJustOnceWithAnyString() { mFoo.add("bob"); - verify(mFoo, LittleMock.times(2)).add(anyString()); + try { + verify(mFoo, LittleMock.times(2)).add(anyString()); + fail(); + } catch (AssertionError expected) {} } - @ShouldThrow(AssertionError.class) public void testVerify_TimesTwice_FailsIfThreeTimesWithAnyString() { mFoo.add("bob"); mFoo.add("jim"); mFoo.add("james"); - verify(mFoo, LittleMock.times(2)).add(anyString()); + try { + verify(mFoo, LittleMock.times(2)).add(anyString()); + fail(); + } catch (AssertionError expected) {} } public void testVerify_Never_Succeeds() { @@ -285,10 +282,12 @@ public class LittleMockTest extends TestCase { verify(mFoo, never()).anInt(); } - @ShouldThrow(AssertionError.class) public void testVerify_Never_FailsIfWasCalled() { mFoo.add("jim"); - verify(mFoo, never()).add("jim"); + try { + verify(mFoo, never()).add("jim"); + fail(); + } catch (AssertionError expected) {} } public void testVerify_Never_PassesIfArgumentsDontMatch() { @@ -308,9 +307,11 @@ public class LittleMockTest extends TestCase { verify(mFoo, atLeastOnce()).add("jim"); } - @ShouldThrow(AssertionError.class) public void testVerify_AtLeastOnce_FailsForNoCalls() { - verify(mFoo, atLeastOnce()).add("jim"); + try { + verify(mFoo, atLeastOnce()).add("jim"); + fail(); + } catch (AssertionError expected) {} } public void testVerify_AtLeastThreeTimes_SuceedsForThreeCalls() { @@ -329,11 +330,13 @@ public class LittleMockTest extends TestCase { verify(mFoo, atLeast(3)).add("jim"); } - @ShouldThrow(AssertionError.class) public void testVerify_AtLeastThreeTimes_FailsForTwoCalls() { mFoo.add("jim"); mFoo.add("jim"); - verify(mFoo, atLeast(3)).add("jim"); + try { + verify(mFoo, atLeast(3)).add("jim"); + fail(); + } catch (AssertionError expected) {} } public void testVerify_AtMostThreeTimes_SuceedsForThreeCalls() { @@ -343,14 +346,16 @@ public class LittleMockTest extends TestCase { verify(mFoo, atMost(3)).add("jim"); } - @ShouldThrow(AssertionError.class) public void testVerify_AtMostThreeTimes_FailsForFiveCalls() { mFoo.add("jim"); mFoo.add("jim"); mFoo.add("jim"); mFoo.add("jim"); mFoo.add("jim"); - verify(mFoo, atMost(3)).add("jim"); + try { + verify(mFoo, atMost(3)).add("jim"); + fail(); + } catch (AssertionError expected) {} } public void testVerify_AtMostThreeTimes_SucceedsForTwoCalls() { @@ -377,20 +382,24 @@ public class LittleMockTest extends TestCase { verify(mFoo, between(2, 4)).add("jim"); } - @ShouldThrow(AssertionError.class) public void testVerify_BetweenTwoAndFour_FailsForOneCall() { mFoo.add("jim"); - verify(mFoo, between(2, 4)).add("jim"); + try { + verify(mFoo, between(2, 4)).add("jim"); + fail(); + } catch (AssertionError expected) {} } - @ShouldThrow(AssertionError.class) public void testVerify_BetweenTwoAndFour_FailsForFiveCalls() { mFoo.add("jim"); mFoo.add("jim"); mFoo.add("jim"); mFoo.add("jim"); mFoo.add("jim"); - verify(mFoo, LittleMock.between(2, 4)).add("jim"); + try { + verify(mFoo, LittleMock.between(2, 4)).add("jim"); + fail(); + } catch (AssertionError expected) {} } public void testDoReturnWhen_SimpleReturn() { @@ -416,14 +425,18 @@ public class LittleMockTest extends TestCase { assertEquals(null, mFoo.get(2)); } - @ShouldThrow(IllegalArgumentException.class) public void testDoReturnWhen_CalledOnString() { - doReturn("first").when("hello").contains("something"); + try { + doReturn("first").when("hello").contains("something"); + fail(); + } catch (IllegalArgumentException expected) {} } - @ShouldThrow(IllegalArgumentException.class) public void testDoReturnWhen_CalledOnNonMock() { - doReturn("first").when(createNotARealMock()).get(0); + try { + doReturn("first").when(createNotARealMock()).get(0); + fail(); + } catch (IllegalArgumentException expected) {} } public void testDoReturnWhen_CanAlsoBeVerified() { @@ -469,10 +482,12 @@ public class LittleMockTest extends TestCase { assertEquals(null, mFoo.anInterface()); } - @ShouldThrow(RuntimeException.class) public void testDoThrow_SimpleException() { doThrow(new RuntimeException()).when(mFoo).aDouble(); - mFoo.aDouble(); + try { + mFoo.aDouble(); + fail(); + } catch (RuntimeException expected) {} } public void testDoThrow_IfItDoesntMatchItIsntThrown() { @@ -480,17 +495,17 @@ public class LittleMockTest extends TestCase { mFoo.aChar(); } - @ShouldThrow(RuntimeException.class) public void testDoThrow_KeepsThrowingForever() { doThrow(new RuntimeException()).when(mFoo).aDouble(); try { mFoo.aDouble(); - fail("Should have thrown a RuntimeException"); - } catch (RuntimeException e) { - // Expected. - } + fail(); + } catch (RuntimeException expected) {} // This second call should also throw a RuntimeException. - mFoo.aDouble(); + try { + mFoo.aDouble(); + fail(); + } catch (RuntimeException expected) {} } public void testDoNothing() { @@ -502,21 +517,25 @@ public class LittleMockTest extends TestCase { verifyZeroInteractions(mFoo); } - @ShouldThrow(AssertionError.class) public void testVerifyZeroInteractions_FailsIfSomethingHasHappened() { mFoo.aBoolean(); - verifyZeroInteractions(mFoo); + try { + verifyZeroInteractions(mFoo); + fail(); + } catch (AssertionError expected) {} } public void testVerifyZeroInteractions_HappyWithMultipleArguments() { verifyZeroInteractions(mFoo, mBar); } - @ShouldThrow(AssertionError.class) public void testVerifyZeroInteractions_ShouldFailEvenIfOtherInteractionsWereFirstVerified() { mFoo.add("test"); verify(mFoo).add("test"); - verifyZeroInteractions(mFoo); + try { + verifyZeroInteractions(mFoo); + fail(); + } catch (AssertionError expected) {} } public void testVerifyEq_Method() { @@ -534,22 +553,28 @@ public class LittleMockTest extends TestCase { verify(mBar).mixedArguments(eq(8), eq("test")); } - @ShouldThrow(AssertionError.class) public void testVerifyEq_MethodFailsIfNotEqual() { mFoo.add("bob"); - verify(mFoo).add(eq("jim")); + try { + verify(mFoo).add(eq("jim")); + fail(); + } catch (AssertionError expected) {} } - @ShouldThrow(AssertionError.class) public void testVerifyEq_MethodFailsIfJustOneIsNotEqual() { mBar.twoStrings("first", "second"); - verify(mBar).twoStrings(eq("first"), eq("third")); + try { + verify(mBar).twoStrings(eq("first"), eq("third")); + fail(); + } catch (AssertionError expected) {} } - @ShouldThrow(AssertionError.class) public void testVerifyEq_MethodFailsIfSameParamsButInWrongOrder() { mBar.twoStrings("first", "second"); - verify(mBar).twoStrings(eq("second"), eq("first")); + try { + verify(mBar).twoStrings(eq("second"), eq("first")); + fail(); + } catch (AssertionError expected) {} } public void testCapture_SimpleCapture() { @@ -622,14 +647,6 @@ public class LittleMockTest extends TestCase { assertEquals(newList("whinny", "jessica"), mCaptureAnotherString.getAllValues()); } - public static <T> List<T> newList(T... things) { - ArrayList<T> list = new ArrayList<T>(); - for (T thing : things) { - list.add(thing); - } - return list; - } - public void testAnyString() { doReturn("jim").when(mFoo).lookup(anyString()); assertEquals("jim", mFoo.lookup("barney")); @@ -641,10 +658,12 @@ public class LittleMockTest extends TestCase { verify(mFoo).takesObject(anyString()); } - @ShouldThrow(AssertionError.class) public void testAnyString_ObjectValue() { mFoo.takesObject(new Object()); - verify(mFoo).takesObject(anyString()); + try { + verify(mFoo).takesObject(anyString()); + fail(); + } catch (AssertionError expected) {} } public void testAnyObject() { @@ -696,11 +715,13 @@ public class LittleMockTest extends TestCase { verifyZeroInteractions(mFoo); } - @ShouldThrow(AssertionError.class) public void testReset_VerifyFailsAfterReset() { mFoo.aByte(); reset(mFoo); - verify(mFoo).aByte(); + try { + verify(mFoo).aByte(); + fail(); + } catch (AssertionError expected) {} } public void testCapture_BothBeforeAndAfter() { @@ -738,7 +759,6 @@ public class LittleMockTest extends TestCase { verify(mFoo).add("yes"); } - @ShouldThrow(IOException.class) public void testDoAction_CanThrowDeclaredException() throws Exception { doAnswer(new Callable<Void>() { @Override @@ -746,10 +766,12 @@ public class LittleMockTest extends TestCase { throw new IOException("Problem"); } }).when(mFoo).exceptionThrower(); - mFoo.exceptionThrower(); + try { + mFoo.exceptionThrower(); + fail(); + } catch (IOException expected) {} } - @ShouldThrow(RuntimeException.class) public void testDoAction_CanThrowUndelcaredException() { doAnswer(new Callable<Void>() { @Override @@ -757,7 +779,10 @@ public class LittleMockTest extends TestCase { throw new RuntimeException("Problem"); } }).when(mFoo).aBoolean(); - mFoo.aBoolean(); + try { + mFoo.aBoolean(); + fail(); + } catch (RuntimeException expected) {} } public void testRunThisIsAnAliasForDoAction() { @@ -796,10 +821,12 @@ public class LittleMockTest extends TestCase { verifyNoMoreInteractions(mFoo, mBar); } - @ShouldThrow(AssertionError.class) public void testVerifyNoMoreInteractions_FailsWhenOneActionWasNotVerified() { mFoo.aBoolean(); - verifyNoMoreInteractions(mFoo, mBar); + try { + verifyNoMoreInteractions(mFoo, mBar); + fail(); + } catch (AssertionError expected) {} } public void testVerifyNoMoreInteractions_SucceedsWhenAllActionsWereVerified() { @@ -809,13 +836,15 @@ public class LittleMockTest extends TestCase { verifyNoMoreInteractions(mFoo); } - @ShouldThrow(AssertionError.class) public void testVerifyNoMoreInteractions_FailsWhenMostButNotAllActionsWereVerified() { mFoo.get(3); mFoo.get(20); mFoo.aByte(); verify(mFoo, atLeastOnce()).get(anyInt()); - verifyNoMoreInteractions(mFoo); + try { + verifyNoMoreInteractions(mFoo); + fail(); + } catch (AssertionError expected) {} } public void testVerifyNoMoreInteractions_ShouldIngoreStubbedCalls() { @@ -824,17 +853,20 @@ public class LittleMockTest extends TestCase { verifyNoMoreInteractions(mFoo); } - @ShouldThrow(IllegalArgumentException.class) public void testMatchers_WrongNumberOfMatchersOnStubbingCausesError() { - doReturn("hi").when(mBar).twoStrings("jim", eq("bob")); + try { + doReturn("hi").when(mBar).twoStrings("jim", eq("bob")); + fail(); + } catch (IllegalArgumentException expected) {} } - @ShouldThrow(IllegalArgumentException.class) public void testMatchers_WrongNumberOfMatchersOnVerifyCausesError() { - verify(mBar).twoStrings("jim", eq("bob")); + try { + verify(mBar).twoStrings("jim", eq("bob")); + fail(); + } catch (IllegalArgumentException expected) {} } - @ShouldThrow(IllegalStateException.class) public void testCreateACaptureButDontUseItShouldFailAtNextVerify() { // If we create a capture illegally, outside of a method call, like so: mCaptureString.capture(); @@ -843,23 +875,29 @@ public class LittleMockTest extends TestCase { // call on the mock object. // Thus we should check in the verify() method that there are *no matchers* on the static // list, as this would indicate a programming error such as the above. - verify(mFoo, anyTimes()).aBoolean(); + try { + verify(mFoo, anyTimes()).aBoolean(); + fail(); + } catch (IllegalStateException expected) {} } - @ShouldThrow(IllegalStateException.class) public void testCreateACaptureButDontUseItShouldFailAtNextVerify_AlsoNoMoreInteractions() { // Same result desired as in previous test. mCaptureString.capture(); - verifyNoMoreInteractions(mFoo); + try { + verifyNoMoreInteractions(mFoo); + fail(); + } catch (IllegalStateException expected) {} } - @ShouldThrow(IllegalStateException.class) public void testCreateACaptureButDontUseItShouldFailAtNextVerify_AlsoZeroInteraction() { mCaptureString.capture(); - verifyZeroInteractions(mFoo); + try { + verifyZeroInteractions(mFoo); + fail(); + } catch (IllegalStateException expected) {} } - @ShouldThrow(IllegalStateException.class) public void testCheckStaticVariablesMethod() { // To help us avoid programming errors, I'm adding a method that you can call from tear down, // which will explode if there is anything still left in your static variables at the end @@ -867,12 +905,17 @@ public class LittleMockTest extends TestCase { // variable (so that the next test won't fail). It should fail if we create a matcher // be it a capture or anything else that is then not consumed. anyInt(); - checkForProgrammingErrorsDuringTearDown(); + try { + checkForProgrammingErrorsDuringTearDown(); + fail(); + } catch (IllegalStateException expected) {} } - @ShouldThrow(IllegalArgumentException.class) public void testCantPassNullToVerifyCount() { - verify(mFoo, null).aBoolean(); + try { + verify(mFoo, null).aBoolean(); + fail(); + } catch (IllegalArgumentException expected) {} } public void testInjectionInNestedClasses() throws Exception { @@ -901,10 +944,12 @@ public class LittleMockTest extends TestCase { verify(mFoo).takesObject(isA(String.class)); } - @ShouldThrow(AssertionError.class) public void testIsA_FailsWithSuperclass() { mFoo.takesObject(new Object()); - verify(mFoo).takesObject(isA(String.class)); + try { + verify(mFoo).takesObject(isA(String.class)); + fail(); + } catch (AssertionError expected) {} } public void testIsA_WillAcceptNull() { @@ -918,49 +963,57 @@ public class LittleMockTest extends TestCase { verify(mFoo).takesBar(isA(BarSubtype.class)); } - @ShouldThrow(AssertionError.class) public void testIsA_MatchSubtypeFailed() { mFoo.takesBar(mBar); - verify(mFoo).takesBar(isA(BarSubtype.class)); + try { + verify(mFoo).takesBar(isA(BarSubtype.class)); + fail(); + } catch (AssertionError expected) {} } - @ShouldThrow(value = AssertionError.class, - messages = {"cannot verify call to", "boolean java.lang.Object.equals(java.lang.Object)"}) public void testVerifyEquals_ShouldFail() { mFoo.equals(null); - verify(mFoo).equals(null); + try { + verify(mFoo).equals(null); + fail(); + } catch (AssertionError expected) {} } - @ShouldThrow(value = AssertionError.class, - messages = {"cannot verify call to", "int java.lang.Object.hashCode()"}) public void testVerifyHashCode_ShouldFail() { mFoo.hashCode(); - verify(mFoo).hashCode(); + try { + verify(mFoo).hashCode(); + fail(); + } catch (AssertionError expected) {} } - @ShouldThrow(value = AssertionError.class, - messages = {"cannot verify call to", "java.lang.String java.lang.Object.toString()"}) public void testVerifyToString_ShouldFail() { mFoo.toString(); - verify(mFoo).toString(); + try { + verify(mFoo).toString(); + fail(); + } catch (AssertionError expected) {} } - @ShouldThrow(value = AssertionError.class, - messages = {"cannot stub call to", "boolean java.lang.Object.equals(java.lang.Object)"}) public void testStubEquals_ShouldFail() { - doReturn(false).when(mFoo).equals(null); + try { + doReturn(false).when(mFoo).equals(null); + fail(); + } catch (AssertionError expected) {} } - @ShouldThrow(value = AssertionError.class, - messages = {"cannot stub call to", "int java.lang.Object.hashCode()"}) public void testStubHashCode_ShouldFail() { - doReturn(0).when(mFoo).hashCode(); + try { + doReturn(0).when(mFoo).hashCode(); + fail(); + } catch (AssertionError expected) {} } - @ShouldThrow(value = AssertionError.class, - messages = {"cannot stub call to", "java.lang.String java.lang.Object.toString()"}) public void testStubToString_ShouldFail() { - doReturn("party").when(mFoo).toString(); + try { + doReturn("party").when(mFoo).toString(); + fail(); + } catch (AssertionError expected) {} } public void testEqualsMethod_DoesntCountAsInteraction() { @@ -1014,7 +1067,7 @@ public class LittleMockTest extends TestCase { + " mFoo.aBoolean()\n" + " at " + singleLineStackTrace(verifyLineNumber) + "\n" + "\n" - + "No method calls happened to this mock\n"; + + "No method calls happened on this mock\n"; assertEquals(expectedMessage, e.getMessage()); } } @@ -1100,23 +1153,29 @@ public class LittleMockTest extends TestCase { } } - @ShouldThrow(IllegalArgumentException.class) public void testDoReturn_ThisShouldFailSinceDoubleIsNotAString() { - doReturn("hello").when(mFoo).aDouble(); + try { + doReturn("hello").when(mFoo).aDouble(); + fail(); + } catch (IllegalArgumentException expected) {} } public void testDoReturn_ThisShouldPassSinceStringCanBeReturnedFromObjectMethod() { doReturn("hello").when(mFoo).anObject(); } - @ShouldThrow(IllegalArgumentException.class) public void testDoReturn_ThisShouldFailSinceObjectCantBeReturnedFromString() { - doReturn(new Object()).when(mFoo).aString(); + try { + doReturn(new Object()).when(mFoo).aString(); + fail(); + } catch (IllegalArgumentException expected) {} } - @ShouldThrow(IllegalArgumentException.class) public void testDoReturn_ThisShouldFailSinceBarIsNotSubtypeOfBarSubtype() { - doReturn(mBar).when(mFoo).aBarSubtype(); + try { + doReturn(mBar).when(mFoo).aBarSubtype(); + fail(); + } catch (IllegalArgumentException expected) {} } public void testDoReturn_ThisShouldPassSinceBarSubTypeIsABar() { @@ -1132,10 +1191,12 @@ public class LittleMockTest extends TestCase { // TODO(hugohudson): 7. Should fix this. // Once we fix the previous test we won't need this one. // I'm just demonstrating that currently this fails with NPE at use-time not at stub-time. - @ShouldThrow(NullPointerException.class) public void testDoReturn_ThisShouldFailBecauseNullIsNotAByte2() { doReturn(null).when(mFoo).aByte(); - mFoo.aByte(); + try { + mFoo.aByte(); + fail(); + } catch (NullPointerException expected) {} } public void testDoReturn_ThisShouldPassSinceNullIsAnObject() { @@ -1155,30 +1216,36 @@ public class LittleMockTest extends TestCase { // TODO(hugohudson): 7. Should fix this to give proper message. // We could at least give a good message saying why you get failure - saying that your string // is not a byte, and pointing to where you stubbed it. - @ShouldThrow(ClassCastException.class) public void testDoAnswer_ThisShouldFailSinceStringIsNotAByte2() { doAnswer(new Callable<String>() { @Override public String call() throws Exception { return "hi"; } }).when(mFoo).aByte(); - mFoo.aByte(); + try { + mFoo.aByte(); + fail(); + } catch (ClassCastException expected) {} } - @ShouldThrow(value = IllegalArgumentException.class, - messages = { " (Bar) mFoo.aBar()" }) public void testDoAnswer_ShouldHaveSimpleNameOnReturnValue() { - doReturn("hi").when(mFoo).aBar(); + try { + doReturn("hi").when(mFoo).aBar(); + fail(); + } catch (IllegalArgumentException expected) {} } - @ShouldThrow(IllegalArgumentException.class) public void testCantCreateMockOfNullType() { - mock(null); + try { + mock(null); + fail(); + } catch (IllegalArgumentException expected) {} } - @ShouldThrow(value = AssertionError.class, - messages = { "Expected exactly 1 call to:", "onClickListener.onClick(Bar)" }) public void testCreateMockWithNullFieldName() { OnClickListener mockClickListener = mock(OnClickListener.class); - verify(mockClickListener).onClick(null); + try { + verify(mockClickListener).onClick(null); + fail(); + } catch (AssertionError expected) {} } public void testDoubleVerifyNoProblems() { @@ -1191,10 +1258,185 @@ public class LittleMockTest extends TestCase { verify(mFoo).aByte(); } - // TODO(hugohudson): 5. Every @ShouldThrow method should be improved by adding test for the - // content of the error message. First augment the annotation to take a String which must - // form a substring of the getMessage() for the Exception, then check that the exceptions - // thrown are as informative as possible. + public void testUnfinishedVerifyCaughtInTearDown_Issue5() { + verify(mFoo); + try { + checkForProgrammingErrorsDuringTearDown(); + fail(); + } catch (IllegalStateException expected) {} + } + + public void testUnfinishedWhenCaughtInTearDown_Issue5() { + doThrow(new RuntimeException()).when(mFoo); + try { + checkForProgrammingErrorsDuringTearDown(); + fail(); + } catch (IllegalStateException expected) {} + } + + public void testUnfinishedVerifyCaughtByStartingWhen_Issue5() { + verify(mFoo, never()); + try { + doReturn(null).when(mFoo).clear(); + fail(); + } catch (IllegalStateException expected) {} + } + + public void testUnfinishedWhenCaughtByStartingVerify_Issue5() { + doThrow(new RuntimeException()).when(mFoo); + try { + verify(mFoo).clear(); + fail(); + } catch (IllegalStateException expected) {} + } + + public void testSecondUnfinishedVerifyShouldFailImmediately() throws Exception { + verify(mFoo); + try { + verify(mFoo); + fail(); + } catch (IllegalStateException expected) {} + } + + public void testSecondUnfinishedWhenShouldFailImmediately() throws Exception { + doReturn(null).when(mFoo); + try { + doReturn(null).when(mFoo); + fail(); + } catch (IllegalStateException expected) {} + } + + public void testVerifyWithTimeout_SuccessCase() throws Exception { + CountDownLatch countDownLatch = new CountDownLatch(1); + invokeBarMethodAfterLatchAwait(countDownLatch); + doReturn(null).when(mFoo).aBar(); + verify(mFoo, never()).aBar(); + countDownLatch.countDown(); + verify(mFoo, timeout(100)).aBar(); + } + + public void testVerifyWithTimeout_FailureCase() throws Exception { + long start = System.currentTimeMillis(); + try { + verify(mFoo, timeout(10)).aBar(); + fail(); + } catch (AssertionError expected) {} + long duration = System.currentTimeMillis() - start; + assertTrue(duration > 5); + } + + public void testVerifyWithTimeoutMultipleInvocations_SuccessCase() throws Exception { + CountDownLatch countDownLatch = new CountDownLatch(1); + invokeBarMethodAfterLatchAwait(countDownLatch); + invokeBarMethodAfterLatchAwait(countDownLatch); + doReturn(null).when(mFoo).aBar(); + verify(mFoo, never()).aBar(); + countDownLatch.countDown(); + verify(mFoo, timeout(100).times(2)).aBar(); + verify(mFoo, timeout(100).atLeast(2)).aBar(); + verify(mFoo, timeout(100).between(2, 4)).aBar(); + verify(mFoo, timeout(100).atLeastOnce()).aBar(); + } + + public void testVerifyWithTimeoutMultipleInvocations_FailureCase() throws Exception { + long start = System.currentTimeMillis(); + mFoo.aBar(); + try { + verify(mFoo, timeout(10).between(2, 3)).aBar(); + fail(); + } catch (AssertionError expected) {} + long duration = System.currentTimeMillis() - start; + assertTrue(duration > 5); + + } + + public void testConcurrentModificationExceptions() throws Exception { + // This test concurrently stubs, verifies, and uses a mock. + // It used to totally reproducibly throw a ConcurrentModificationException. + Future<?> task1 = mExecutorService.submit(new Runnable() { + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + mFoo.aBar(); + Thread.yield(); + } + } + }); + Future<?> task2 = mExecutorService.submit(new Runnable() { + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + verify(mFoo, anyTimes()).aBar(); + Thread.yield(); + } + } + }); + Thread.sleep(20); + task1.cancel(true); + task2.cancel(true); + try { + task1.get(); + fail(); + } catch (CancellationException expected) {} + try { + task2.get(); + fail(); + } catch (CancellationException expected) {} + } + + public void testCannotVerifyFromSecondThreadAfterStubbingInFirst() throws Exception { + doReturn(null).when(mFoo).aBar(); + Future<?> submit = mExecutorService.submit(new Runnable() { + @Override + public void run() { + verify(mFoo, anyTimes()).aBar(); + } + }); + try { + submit.get(); + fail(); + } catch (ExecutionException expected) { + assertTrue(expected.getCause() instanceof IllegalStateException); + } + } + + public void testCannotStubFromSecondThreadAfterVerifyingInFirst() throws Exception { + mExecutorService.submit(new Runnable() { + @Override + public void run() { + verify(mFoo, anyTimes()).aBar(); + } + }).get(); + try { + doReturn(null).when(mFoo).aBar(); + fail(); + } catch (IllegalStateException expected) {} + } + + public class Jim { + public void bob() { + fail(); + } + } + + public void testMockingConcreteClasses() throws Exception { + Jim mock = mock(Jim.class); + mock.bob(); + } + + private Future<Void> invokeBarMethodAfterLatchAwait(final CountDownLatch countDownLatch) { + return mExecutorService.submit(new Callable<Void>() { + @Override + public Void call() throws Exception { + countDownLatch.await(); + mFoo.aBar(); + return null; + } + }); + } + + // 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 @@ -1258,14 +1500,8 @@ public class LittleMockTest extends TestCase { // //then // assertThat(goods, containBread()); - // TODO(hugohudson): 6. How about timeouts that are mentioned in Mockito docs? - // Sounds like a good idea. Would look like this: - // verify(mFoo, within(50).milliseconds()).get(0); - - // TODO(hugohudson): 6. Consider bringing back in the async code I wrote for the AndroidMock - // framework so that you can expect to wait for the method call. - // Although obviously we're more keen on encouraging people to write unit tests that don't - // require async behaviour in the first place. + // TODO: All unfinished verify and when statements should have sensible error messages telling + // you where the unfinished statement comes from. /** Returns the line number of the line following the method call. */ private int getNextLineNumber() { @@ -1307,4 +1543,12 @@ public class LittleMockTest extends TestCase { } }; } + + private static <T> List<T> newList(T... things) { + ArrayList<T> list = new ArrayList<T>(); + for (T thing : things) { + list.add(thing); + } + return list; + } } |