Saturday, March 13, 2010

Followup on Mockito thread safety

In a previous blog post, I wrote that Mockito could not share mocks in a thread safe way, but I didn't motivate it in much detail.

Here's the sample code that fails:

import org.junit.Test;
import org.mockito.InOrder;
import org.mockito.Mockito;

import java.util.List;
import java.util.concurrent.CountDownLatch;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.*;

public class TestMulti {
private static interface TestInterface {
int foo(String threadName, int count);
}

@Test
public void testSharedMock() throws InterruptedException {
final CountDownLatch start = new CountDownLatch(1);
final CountDownLatch stop = new CountDownLatch(1);

final int numThreads = 2;
final long TIMEOUT = 2000;

final TestInterface mock = Mockito.mock(TestInterface.class);

Thread[] threads = new Thread[numThreads];
final int[] counts = new int[numThreads];
for (int i = 0; i < numThreads; i++) {
final String name = "Thread:" + i;
final int finalI = i;
Thread t = new Thread(name) {
@Override
public void run() {
try {
start.await();
long t1 = System.currentTimeMillis();
int count = 0;
while (System.currentTimeMillis() - t1 < TIMEOUT) {
count++;
doReturn(count).when(mock).foo(name, count);
verify(mock, never()).foo(name, count);
assertEquals(count, mock.foo(name, count));
verify(mock).foo(name, count);
}
counts[finalI] = count;
System.out.println(name + " did " + count + " iterations");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
stop.countDown();
}
}
};
threads[i] = t;
t.start();
}


start.countDown();
stop.await();
int totalCount = 0;
for (int i = 0; i < numThreads; i++) {
final String name = "Thread:" + i;
int count = counts[i];
totalCount += count;
InOrder order = inOrder(mock);
for (int j = 0; j < count; j++) {
order.verify(mock).foo(name, 1 + j);
}
verify(mock, times(count)).foo(eq(name), anyInt());
}
System.out.println("Total count: " + totalCount);
}
}



Due to the undeterministic behaviour, this can error with multiple problems, including:
  1. Exception in thread "Thread:1" java.lang.NullPointerException
    at org.mockito.internal.stubbing.MockitoStubber.findAnswerFor(MockitoStubber.java:63)
  2. Exception in thread "Thread:1" java.lang.AssertionError: expected:<1> but was:<0>
  3. Exception in thread "Thread:1" org.mockito.exceptions.misusing.UnfinishedVerificationException:
    Missing method call for verify(mock) here:
  4. Exception in thread "Thread:0" org.mockito.exceptions.misusing.UnfinishedStubbingException:
    Unfinished stubbing detected here:
    -> at TestMulti$1.run(TestMulti.java:43)
  5. Exception in thread "Thread:0" java.util.ConcurrentModificationException
    at java.util.LinkedList$ListItr.checkForComodification(LinkedList.java:761)
    at java.util.LinkedList$ListItr.next(LinkedList.java:696)
    at org.mockito.internal.stubbing.MockitoStubber.findAnswerFor(MockitoStubber.java:62)
  6. Exception in thread "Thread:0" java.util.ConcurrentModificationException
    at java.util.LinkedList$ListItr.checkForComodification(LinkedList.java:761)
    at java.util.LinkedList$ListItr.next(LinkedList.java:696)
    at org.mockito.internal.stubbing.MockitoStubber.findAnswerFor(MockitoStubber.java:62)
  7. Exception in thread "Thread:0" java.lang.NullPointerException
    at java.util.concurrent.ConcurrentLinkedQueue.offer(ConcurrentLinkedQueue.java:190)
    at java.util.concurrent.ConcurrentLinkedQueue.add(ConcurrentLinkedQueue.java:180)
    at org.mockito.internal.stubbing.StubbedInvocationMatcher.addAnswer(StubbedInvocationMatcher.java:34)