W ramach kursu Javy było już o testach jednostkowych jako takich — dzisiaj niejako uzupełnienie tej lekcji, czyli o biblitece Mockito.
Biblioteka ta służy do wygodnego ‘mockowania’ obiektów, czyli innymi słowy ‘udawania’ — dzięki temu możemy przetestować wybrany komponent, dostarczając mu ‘udawane’ zależności, przez co nie musimy np. tworzyć testowej bazy danych czy obawiać się, że jakies dane zostaną zmienione podczas testów. Daje też wiele dodatkowych możliwości, które możemy wykorzystać w naszych testach — ale o tym trochę dalej.
W tym miejscu warto jeszcze powiedzieć, dlaczego mockować? Przecież można po prostu użyć serwisów lub napisać ich implementację tylko na potrzeby testów. Odpowiedź brzmi — oczywiście, że można, tylko po co? Mockowanie daje nam przewidywalny wynik działania konkretnego serwisu, niezależny od np. połączenia z innymi systemami, danych w bazie danych czy innych okoliczności. Dzięki temu unikamy sytuacji w której testy dają różne wyniki w zależności od tego, kiedy, jak i na jakim urządzeniu zostaną urochomione.
Druga zaleta to możliwość rekonfiguracji — dodatkowa implementacja jakiegoś serwisu tylko na potrzeby testu jest ograniczona jeśli chodzi o skrajne przypadki. Mocki pozwalają nam zmieniać zachowanie mocków pomiędzy testami, czyniąc je wyjątkowo elastycznym i przydatnym narzędziem do testów.
Zanim zaczniemy
Pobieranie bibliteki
Bibliotekę Mockito najłatwiej pobrać z centralnego repozytorium Maven — interesuje nas artefakt org.mockito:mockito-all.
Źródła omawianych klas
Na potrzeby tego wpisu załóżmy, że mamy np. klasę NotificationService . Korzysta ona z implementacji 2 interfejsów — SMSService oraz EmailService, oraz udostępnia jedną metodę sendNotification (która w założeniu powinna wywołać metody obu tych interfejsów, jeśli podamy odpowiednie dane). Nasze klasy wyglądają następująco:
NotificationService
public interface NotificationService {
public void sendNotification(Optional email, Optional phoneNumber, String notificationContent);
}
NotificationServiceImpl
public class NotificationServiceImpl implements NotificationService {
public NotificationServiceImpl(EmailService emailService, SMSService smsService) {
this.emailService = emailService;
this.smsService = smsService;
}
private SMSService smsService;
private EmailService emailService;
public void sendNotification(Optional email, Optional phoneNumber, String notificationContent) {
if (email.isPresent()) {
emailService.sendMessage(email.get(), notificationContent);
}
if (phoneNumber.isPresent()) {
smsService.sendMessage(phoneNumber.get(), notificationContent);
}
}
}
SMSService
public interface SMSService {
public SMSMessage sendSMS(String phoneNumber, String content);
}
EmailService
public interface EmailService {
public EmailMessage sendEmail(String email, String content);
}
Testy jednostkowe z użyciem mocków
Inicjowanie mocków
Najwyższy czas zacząć testować! Mockito pozwala na dwa ‘tryby’ rozpoczęcia pracy — deklarując odpowiednią klasę wykonującą testy (za pomocą adnotacji @RunWith) oraz wywołując metodę initMocks np. w konstruktorze klasy. Gorąco zalecamy korzystać z adnotacji, ale przedstawimy obie te metody. Funkcjonalnie nie ma pomiędzy nimi różnicy, różnią się jedynie czytelnością zapisu.
Inicjowanie za pomocą adnotacji
@RunWith(MockitoJUnitRunner.class)
public class NotificationServiceTest {
@Mock
EmailService emailService;
/* deklaracja innych mocków */
}
Inicjowanie za pomocą metody initMocks
public class NotificationServiceTest {
@Mock
EmailService emailService;
/* deklaracja innych mocków */
public NotificationServiceTest() {
MockitoAnnotations.initMocks(this);
}
}
Inicjowanie testowanego obiektu z użyciem mocków
Są dwa podejścia do tego, które zależą od sposobu, w jaki tworzysz klasy.
Mockowanie pól z adnotacją @Autowired
Metoda ta sprawdzi się, jeśli wszystkie pola testowanej klasy mają adnotację @Autowired, i chcemy korzystać tylko z mocków. Wtedy wystarczy użyć adnotacji @InjectMocks, jak na poniższym przykładzie:
@RunWith(MockitoJUnitRunner.class)
public class NotificationServiceTest {
@Mock
EmailService emailService;
@Mock
SMSService smsService;
@InjectMocks
NotificationServiceImpl notificationService;
}
Pomimo, że jest to wspierany przez Mockito sposób, zalecane jest używanie drugiej metody
Wstrzykiwanie za pomocą konstruktora
Drugi sposób to wstrzyknięcie zależności poprzez konstruktor. Jest to preferowana metoda (dla zainteresowanych polecam artykuł na blogu tedvinke.wordpress.com).
public class NotificationServiceTest {
@Mock
EmailService emailService;
@Mock
SMSService smsService;
NotificationService notificationService;
@Before
public void setUp() {
notificationService = new NotificationServiceImpl(emailService, smsService);
}
}
Zwróć uwagę, że w ten sposób obiekt będzie tworzony przed każdym testem od nowa. To raczej zalecana praktyka (dzięki temu eliminujemy możliwą ‘stanowość’ serwisu pomiędzy testami), natomiast jeśli samo tworzenie serwisu jest czasochłonne, wymaga dużej ilości pamięci lub też testowany obiekt nie może być w wielu instancjach, można to zachowanie zmodyfikować wg potrzeb.
Konfiguracja mocków
Mając już poprawnie zainicjowane mocki możemy przystąpić do ich konfiguracji. Możemy to zrobić w dwóch miejscach — albo w metodzie z adnotacją @Before albo w konkretnym teście. Różnica jest taka, że metoda z adnotacją @Before spowoduje, że wszystkie testy w tej klasie będą korzystały z tej konfiguracji, konfiguracja w metodzie testujacej oczywiście będzie ‘widoczna’ tylko w ramach tego jednego testu. Można jednak nadpisywać konfigurację — dlatego warto w metodzie z adnotacją @Before skonfigurować ‘domyślne’ zachowanie mocków, i ew w zależności od potrzeby modyfikować je w pojedynczych testach.
Co ważne — nie musimy konfigurować wszystkich metod mocka! Wystarczy skonfigurować te, które będą wykorzystywane (pośrednio oczywiście) przez nasze testy.
Konfiguracja obiektu zwracanego z metody — statyczny obiekt
Najprostszy sposób konfiguracji danej metody mocka to zwrócenie statycznego obiektu dla każdego wywołania tej metody. To oczywiście nie zawsze dobra praktyka, ale bardziej rozbudowane możliwości omówimy za chwilę. Aby zwrócić obiekt dla każdego wywołania metody niezależnie od argumentów, używamy konstrukcji:
EmailMessage message = ... //tutaj inicjujemy obiekt, który chcemy zwracać, ustawiamy wartości pól itp
Mockito.when(emailService.sendEmail(Matchers.anyObject(), Matchers.anyObject())).thenReturn(message);
Matchers.anyObject() oznacza, że dla dowolnego obiektu podanego jako argument. O innych możliwościach w tym zakresie powiemy sobie za chwilę.
Powyższy kod można przeczytać jako:
Kiedy wywołana zostanie metoda sendEmail mocka emailService, z dowolnymi argumentami, zwróć obiekt message.
Metody nie zwracające wartości
Szczególnym przypadkiem są metody, które nie zwracają żadnego obiektu, powyższa składnia oczywiście nie zadziała, ale istnieje sposób, aby to obejść (na potrzeby tego przykładu załóżmy, że metoda sendEmail nie zwraca niczego):
Mockito.doNothing().when(emailService).sendEmail(Matchers.anyObject(), Matchers.anyObject()));
Ta składnia może być używana także do mockowania ‘standardowych’ metod, ale jest mniej czytelna od omawianych wyżej metod.
Konfiguracja parametrów metody
Mockito pozwala nam konfigurować zachowanie metody także w zależności od tego, z jakimi argumentami wywołamy daną metodę. Spójrzmy na poniższy przykład:
EmailMessage message = ... ;
EmailMessage adminMessage = ... ;
Mockito.when(emailService.sendEmail(Matchers.anyObject(), Matchers.anyObject())).thenReturn(message);
Mockito.when(emailService.sendEmail(Matchers.eq("[email protected]"), Matchers.anyObject())).thenReturn(adminMessage);
Powyższy kod można przeczytać jako:
Kiedy wywołana zostanie metoda sendEmail mocka emailService, z dowolnymi argumentami, zwróć obiekt message, ale kiedy pierwszy argument jest równy (równy w sensie ‘equals’) “[email protected]”, zwróć obiekt adminMessage.
To bardzo elastyczne rozwiązanie, a ilość możliwych sprawdzeń, jakie mamy dostępne jest ogromna (pełną listę znajdzies w dokumentacji API klasy Matchers). Z ważniejszych warto wspomnieć o isA (klasa określonego typu), isNotNull (argument nie jest nullem), startsWith (ciąg znaków zaczyna się od …).
To tylko jedna opcja, jeśli chodzi o zmianę zachowania w zależności od argumentów. Możemy także wykonać określony kod z użyciem wspomnianych argumentów, który wygeneruje obiekt odpowiedzi — o tym powiemy sobie trochę dalej.
Konfiguracja obiektu zwracanego z wykorzystaniem argumentów wywołania metody
Poza zwracaniem statycznego obiektu, Mockito pozwala nam także generować obiekt zwracany na podstawie przekazywanych argumentów. Jest to szczególnie przydatne, jeśli mockujemy interfejsy DAO. Spójrzmy na poniższy przykład:
Mockito.when(emailService.sendEmail(Matchers.anyObject(), Matchers.anyObject())).thenAnswer(new Answer<EmailMessage>() {
public EmailMessage answer(InvocationOnMock invocation) throws Throwable {
return new EmailMessage(
(String) invocation.getArguments()[0],
(String) invocation.getArguments()[1]
);
}
});
Powyższy kod można przeczytać jako:
Kiedy wywołana zostanie metoda sendEmail mocka emailService, z dowolnymi argumentami, zwróć obiekt EmailMessage tworząc go przekazując jako argumenty konstruktora argumenty użyte do wywołania metody.
Oczywiście kod ten można nieco uprościć używając lambdy, ale o niej będzie dopiero w kolejnych wpisach :)
Rzucanie wyjątków w mockach
Na potrzeby testów czasem chcemy, aby metoda rzuciła wyjątek. Oczywiście z Mockito nie ma żadnego problemu, jak na poniższym przykładzie:
RuntimeException exception = ... ;
Mockito.when(emailService.sendEmail(Matchers.anyObject(), Matchers.anyObject())).thenThrow(exception);
Powyższy kod można przeczytać jako:
Kiedy wywołana zostanie metoda sendEmail mocka emailService, z dowolnymi argumentami, rzuć wyjątek typu RuntimeException.
Weryfikacja informacji z mocka
Mocki mogą nam służyć nie tylko do ‘udawania’ obiektów, ale także do weryfikowania — możemy np. sprawdzić, ile razy dana metoda była uruchomiona lub też z jakimi parametrami została uruchomiona. Dzięki temu możemy rozszerzyc testy o weryfikację, czy komunikacja z zależnościami faktycznie występuje.
Weryfikacja ile razy dana metoda mocka zostala wywołana
Mockito pozwala nam na prostą weryfikację, ile razy metoda została użyta. Weryfikacja ta rządzi się podobnymi prawami, co konfiguracja mocków — możemy określić warunki, jakie mają spełniać argumenty, aby zliczenie nastąpiło. Mechanizm ten omówiony jest szerzej we wczesniejszych sekcjach, tutaj ograniczymy się do prostego przykładu.
Mockito.verify(emailService, Mockito.times(1)).sendEmail(Matchers.anyObject(), Matchers.anyObject());
Powyższa linijka sprawdzi, czy metoda sendEmail została wywołana z dowolnymi argumentami dokładnie raz.
Mockito.verify(emailService, Mockito.atLeast(1)).sendEmail(Matchers.startsWith("admin@"), Matchers.anyObject());
Ta linijka z kolei weryfikuje, czy metoda sendEmail została wywołana z pierwszym parametrem zaczynającym się od ‘admin@’ co najmniej raz.
Weryfikacja argumentów, z jakimi metoda mocka została wywołana
W testach możemy także weryfikować, jakie argumenty zostały użyte do wywołania mocka. Dzięki temu możliwe jest sprawdzenie, czy np. argument został prawidłowo przetworzony przed przekazaniem do mocka. W poniższym przykładzie sprawdzamy jeden z argumentów metody:
ArgumentCaptor<String> emailCaptor = ArgumentCaptor.forClass(String.class);
Mockito.verify(emailService).sendEmail(emailCaptor.capture(), Matchers.anyObject());
//wykonaj jakąś akcję na testowanym obiekcie
Assert.assertEquals("[email protected]", emailCaptor.getValue());
Powyższa linijka sprawdzi, czy metoda sendEmail została wywołana z argumentem “[email protected]” przekazanym jako pierwszy. Oczywiście możemy także ‘przechwytywać’ więcej argumentów:
ArgumentCaptor<String> emailCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<String> contentCaptor = ArgumentCaptor.forClass(String.class);
Mockito.verify(emailService).sendEmail(emailCaptor.capture(), contentCaptor.capture());
//wykonaj jakąś akcję na testowanym obiekcie
Assert.assertEquals("[email protected]", emailCaptor.getValue());
Assert.assertNotEquals("", contentCaptor.getValue());
Możemy także weryfikować argumenty, jeśli wywołanie nastąpiło więcej niż jeden raz:
ArgumentCaptor<String> emailCaptor = ArgumentCaptor.forClass(String.class);
Mockito.verify(emailService).sendEmail(emailCaptor.capture(), Matchers.anyObject());
//wykonaj jakąś akcję na testowanym obiekcie
List<String> emaile = emailCaptor.getAllValues();
Podsumowanie
Znajomość Mockito wprawdzie rzadko kiedy jest wymagana na stanowisku Juniorskim, niemniej jest to bardzo przydatna biblioteka i jej znajomość z pewnością będzie plusem dla Ciebie w większości firm.
Biblioteka Mockito dzięki elastyczności jest de-facto standardem jesli chodzi o testy jednostkowe w projektach i umiejętne korzystanie z niej zaoszczędzi Ci wiele czasu i nerwów ;)