Skip to content

Mockito – testowanie asynchronicznych wywołań

przez Łukasz Picur - Sierpień 14th, 2011

W jednym z poprzednich artykułów opisywałem podstawy korzystania z jednego z najpopularniejszych frameworków służących do mockowania – Mockito. Poruszyłem w nim najważniejsze i najpopularniejsze kwestie, jakie wiążą się z tym tematem. Dziś natomiast chciałbym wspomnieć o problemie, z którym spotkać można się dość często. Mowa o testowaniu asynchronicznych wywołań, i o tym jak Mockito może nam w tym pomóc. Zapraszam do dalszej lektury.

Test można zazwyczaj podzielić na trzy logiczne części:

  1. Stworzenie odpowiednich warunków, w jakich test będzie się odbywał.
  2. Wywołanie testowanej funkcjonalności w skonstruowanym wcześniej środowisku.
  3. Sprawdzenie, czy użycie testowanej funkcjonalności dało oczekiwane rezultaty.

Sekcje te wykonywane są sekwencyjnie – jedna po drugiej. Jak więc w ten model wpasowuje się testowanie metod, które uruchamiają dodatkowe wątki? Sytuację najlepiej zobrazować na przykładzie. Poniżej przedstawione są klasy, które będą brały udział w teście – pierwsza z nich to prosty silnik, którego celem jest przetwarzanie tekstu:

package eu.jabablog.mockito.concurrent.example.processing;

import java.util.ArrayList;
import java.util.List;

public class ProcessingEngine
{
	private List listeners = new ArrayList();

	public void addProcessingListener(ProcessingListener listener)
	{
		listeners.add(listener);
	}

	public void removeProcessingListener(ProcessingListener listener)
	{
		listeners.remove(listener);
	}

	public String process(String input)
	{
		for(ProcessingListener listener : listeners)
		{
			ProcessingNotificationTask task = new ProcessingNotificationTask(listener, input);
			new Thread(task).start();
		}

		return input.toUpperCase();
	}
}

Jak widać, daje ona możliwość rejestrowania listenerów, które powiadamiane są o procesie przetwarzania. Muszą one implementować interfejs ProcessingListener, który posiada tylko jedna metodę – onProcess.

package eu.jabablog.mockito.concurrent.example.processing;

public interface ProcessingListener
{
	void onProcess(String input);
}

Ważną cechą implementacji klasy ProcessingEngine jest to, że metody onProcess listenerów uruchamiane są w nowym wątku. Wykorzystywana jest do tego celu klasa ProcessingNotificationTask, która implementuje interfejs Runnable i służy do utworzenia nowego wątku.

package eu.jabablog.mockito.concurrent.example.processing;

import java.util.logging.Logger;

public class ProcessingNotificationTask implements Runnable
{
	private ProcessingListener listener;

	private String input;

	private Logger logger = Logger.getLogger(this.getClass().getName());

	public ProcessingNotificationTask(ProcessingListener listener, String input)
	{
		this.listener = listener;
		this.input = input;
	}

	public void run()
	{
		String message = String.format("Invoking listener: <%s>", listener.getClass().getName());
		logger.info(message);

		listener.onProcess(input);
	}
}

Spróbujmy więc przetestować metodę process klasy ProcessingEngine pod kątem powiadamiania zarejestrowanych listenerów. Korzystając z Mockito, można to zrobić w bardzo prosty sposób – tworzymy mocka listenera, rejestrujemy go w obiekcie klasy ProcessingEngine, uruchamiamy metodę process dla testowych danych, a na koniec sprawdzamy, czy metoda onProcess listenera została wywołana. Bez większego namysłu można napisać kod podobny do poniższego.

@Test
public void testProcess()
{
	// given
	ProcessingEngine engine = new ProcessingEngine();
	ProcessingListener listener = mock(ProcessingListener.class);

	engine.addProcessingListener(listener);

	// when
	engine.process(TEST_INPUT);

	// then
	verify(listener, times(1)).onProcess(anyString());
}

Na pierwszy rzut oka wygląda dobrze, jednak już po którymś z kolei uruchomieniu testu okazuje się, że coś jest nie tak. Mockito informuje nas, że metoda onProcess nie została wywołana. Jak to możliwe? Wyjaśnienie jest bardzo proste – metoda ta wywoływana jest w osobnym wątku. Wątek ten najwyraźniej nie zdążył się zakończyć przed wywołaniem metody verify. Problem ten jest o tyle nieprzyjemny, że może nie ujawnić się od razu. Uruchamiając testy lokalnie na własnej maszynie wszystko może być w najlepszym porządku. O błędzie możemy się dowiedzieć jakiś czas później (najczęściej w chwili opuszczania miejsca pracy), gdy dostajemy maila z informacją o zepsutym buildzie. Aby test działał poprawnie, należy więc zsynchronizować wątki tak, aby weryfikacja ilości wywołań metody onProcess odbyła się dopiero po jej wywołaniu.

@Test
public void testProcess() throws InterruptedException
{
	// given
	ProcessingEngine engine = new ProcessingEngine();
	ProcessingListener listener = mock(ProcessingListener.class);

	final CountDownLatch latch = new CountDownLatch(1);
	doAnswer(new Answer()
	{
		public Void answer(InvocationOnMock invocation) throws Throwable
		{
			latch.countDown();
			return null;
		}
	}).when(listener).onProcess(anyString());
	engine.addProcessingListener(listener);

	// when
	engine.process(TEST_INPUT);
	latch.await();

	// then
	verify(listener, times(1)).onProcess(anyString());
}

Do wspomnianej synchronizacji posłużył obiekt klasy CountDownLatch. Tym sposobem mamy pewność, że sprawdzenie ilości wywołań metody onProcess będzie wykonane dopiero po zakończeniu wątku, w którym metoda ta jest używana. Pisząc testy jednostkowe należy zwracać baczną uwagę na kod, który wykonuje się asynchronicznie, a w przypadku testowania go skorzystać z opisanej powyżej techniki. Pozwoli to zaoszczędzić dużo czasu i zszarganych nerwów.

Kategoria → Java

3 komentarzy
  1. Ciekawy post, dzięki!

    Wydaje się że możesz odpuścić times(1) bo jest to defaultowa wartość, więc samo
    verify(listener).onProcess(anyString());
    powinno wystarczyć.

    Zastanawiałem się czy można by jakoś pominąć w testach te niskopoziomowe zabawy z CountDownLatch. Od pewnego czasu przymierzam się do biblioteki Awaitility (patrz http://code.google.com/p/awaitility/wiki/Usage) ale wydaje się, że w przypadku weryfikacji wywołań nie pomoże ona zbytnio.

    • Dzięki!

      Co do times(1) to rzeczywiście, można sobie odpuścić. Z zasady wolę jednak, aby przykłady były nawet lekko nadmiarowe. Dzięki temu kod lepiej pokazuje możliwości narzędzia, i dodatkowo jest bardziej zrozumiały dla kogoś, kto ogląda go pierwszy raz. Takie jest przynajmniej moje zdanie :)

      Co do awaitility, to wielkie dzięki za linka. Przyjrzę się temu toolowi pod kątem zastosowania w przykładach z wpisu i postaram się podzielić swoimi przemyśleniami.

Trackbacks & Pingbacks

  1. Awaitility – łatwiejsze testy asynchronicznego kodu? | Java Blog

Zostaw komentarz

Info: XHTML jest dozwolony. Twój adres email nigdy nie będzie opublikowany.

Obserwuj komentarze poprzez RSS