Mock objektumok többszálú használata

A lenti EasyMock (3.0) tesztben átadunk az executornak egy Runnable példányt. Ez egy mock objektum, és talán azt várnánk, hogy a verify() majd jelzi, hogy az executor (egy másik szálban) engedély nélkül meghívta a mock objektum run() metódusát, azaz hibával elszáll a teszt.

import static org.junit.Assert.assertTrue;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.easymock.EasyMock;
import org.junit.Test;

public class EasyMockTest {

	@Test
	public void testEasyMock() throws Exception {
		final Runnable runnable = EasyMock.createMock(Runnable.class);
		EasyMock.makeThreadSafe(runnable, true); // default true
		EasyMock.replay(runnable);

		runInNewThread(runnable);

		EasyMock.verify(runnable); // should fail
	}

	private void runInNewThread(final Runnable runnable)
			throws InterruptedException {
		final ExecutorService executor = Executors.newSingleThreadExecutor();
		executor.execute(runnable);

		executor.shutdown();
		final boolean terminated = executor.awaitTermination(2,
				TimeUnit.SECONDS);
		assertTrue("terminated", terminated);
	}
}

Nem ez történik, a teszt hiba nélkül lefut, a verify() nem jelez hibát. A konzolon viszont megjelenik a hibaüzenet:

Exception in thread "pool-1-thread-1" java.lang.AssertionError: 
  Unexpected method call run():
	at org.easymock.internal.MockInvocationHandler.invoke(MockInvocationHandler.java:45)
	at org.easymock.internal.ObjectMethodsFilter.invoke(ObjectMethodsFilter.java:73)
	at $Proxy4.run(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
	at java.lang.Thread.run(Thread.java:619)

JMock-kal (2.5.1) ugyanez a helyzet:

import static org.junit.Assert.assertTrue;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.jmock.Mockery;
import org.jmock.integration.junit4.JMock;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(JMock.class)
public class JmockTest {

	private Mockery mockery;

	@Before
	public void setUp() {
		mockery = new Mockery();
	}

	@Test // should fail
	public void testJmock() throws Exception {
		final Runnable runnable = mockery.mock(Runnable.class);

		runInNewThread(runnable);
	}

	...
}

A teszt sikeresen lefut, a hibaüzenet itt is csak a hibakonzolon jelenik meg.

Mockitót (1.8.5) használva jobbak az eredmények:

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.junit.Test;

public class MockitoTest {

	@Test
	public void testMociko() throws Exception {
		final Runnable runnable = mock(Runnable.class);

		runInNewThread(runnable);

		try {
			verify(runnable, never()).run(); // failed
			fail("verify");
		} catch (final AssertionError expected) {

		}
	}

	...
}

A verify() hívás hibát dob, ahogy vártuk. A konzolon pedig nem jelenik meg semmi, mert mock() metódus úgynevezett nice mock objektumokat készít, amelyek csak rögzítik a meghívott metódusaikat és sosem dobnak hibát.

Az EasyMock-hoz egy nem túl szép workaround saját ThreadFactory használata a kivételt elkapó UncaughtExceptionHandler-rel, majd a teszt végén az UncaughtExceptionHandler ellenőrzése.

import static org.junit.Assert.assertTrue;

import java.lang.Thread.UncaughtExceptionHandler;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.easymock.EasyMock;
import org.junit.Test;

public class EasyMockExceptionHandlerTest {

	@Test
	public void testEasyMockWithUncaughtExceptionHandler() throws Exception {
		final Runnable runnable = EasyMock.createMock(Runnable.class);
		EasyMock.makeThreadSafe(runnable, true); // default true
		EasyMock.replay(runnable);

		final AtomicBoolean threadError = new AtomicBoolean(false);
		final ExecutorService executor = createExecutor(threadError);
		executor.execute(runnable);

		executor.shutdown();
		final boolean terminated = executor.awaitTermination(2,
				TimeUnit.SECONDS);
		assertTrue("terminated", terminated);

		EasyMock.verify(runnable);
		assertTrue("thread error", threadError.get());
	}

	private ExecutorService createExecutor(final AtomicBoolean threadError) {
		final ErrorFlagUncaughtExceptionHandler uncaughtExceptionHandler 
			= new ErrorFlagUncaughtExceptionHandler(threadError);

		final ThreadFactory threadFactory = new ThreadFactory() {
			final ThreadFactory defaultThreadFactory = Executors
					.defaultThreadFactory();

			public Thread newThread(final Runnable runnable) {
				final Thread thread = defaultThreadFactory.newThread(runnable);
				Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
				return thread;
			}
		};
		final ExecutorService executor = Executors
				.newSingleThreadExecutor(threadFactory);
		return executor;
	}

	private class ErrorFlagUncaughtExceptionHandler implements
			UncaughtExceptionHandler {

		private final AtomicBoolean threadError;

		public ErrorFlagUncaughtExceptionHandler(final AtomicBoolean threadError) {
			this.threadError = threadError;
		}

		public void uncaughtException(final Thread thread,
				final Throwable throwable) {
			threadError.set(true);
		}
	}
}

JMock-hoz talán a 2.6-os vagy 2.7-es verzióban lesz erre megoldás, a jelenlegi dokumentáció alapján nem igazán támogatott a JMock mock objektumainak többszálú használata. Lásd még: JMOCK-213,
JMOCK-183.

EasyMock: During the replay phase, mocks are by default thread-safe.

Ugyanez a helyzet a Mockitónál is: You can let multiple threads call methods on a shared mock to test in concurrent conditions.

Az igazi megoldást azonban nem mock-olásra használt osztálykönyvtárban kellene implementálni, hanem a tesztelt kódban, hogy a más szálakon keletkező hibákról a hívó értesüljön, valamint a tesztünk is ezen az úton tudja meg, hogy hiba történt. Az EasyMock levelezőlistáján olvasható erről egy levél:

„Let's say the exception would be thrown by a real implementation, and *not* by EasyMock. How would your application know that the calculation went wrong?”

Erre használható például az ExecutorService:

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.easymock.EasyMock;
import org.junit.Test;

public class ExecutorServiceTest {

	@Test(expected = ExecutionException.class)
	public void testEasyMockWithExecutorService() throws Exception {
		final Runnable runnable = EasyMock.createMock(Runnable.class);
		EasyMock.makeThreadSafe(runnable, true); // default true
		EasyMock.replay(runnable);

		final ExecutorService executorService = Executors
				.newSingleThreadExecutor();

		final Future future = executorService.submit(runnable);
		future.get();
	}

}
Tartalom átvétel