#Niezbędnik Juniora. Mockito

By 2 September 2015 April 24th, 2019 Niezbędnik Juniora

W ramach kur­su Javy było już o tes­tach jed­nos­tkowych jako takich — dzisi­aj nie­jako uzu­pełnie­nie tej lekcji, czyli o bib­litece Mockito. 

Bib­liote­ka ta służy do wygod­nego ‘mock­owa­nia’ obiek­tów, czyli inny­mi słowy ‘udawa­nia’ — dzię­ki temu może­my przetestować wybrany kom­po­nent, dostar­cza­jąc mu ‘udawane’ zależnoś­ci, przez co nie musimy np. tworzyć testowej bazy danych czy obaw­iać się, że jakies dane zostaną zmienione pod­czas testów. Daje też wiele dodatkowych możli­woś­ci, które może­my wyko­rzys­tać w naszych tes­tach — ale o tym trochę dalej.

W tym miejs­cu warto jeszcze powiedzieć, dlaczego mock­ować? Prze­cież moż­na po pros­tu użyć ser­wisów lub napisać ich imple­men­tację tylko na potrze­by testów. Odpowiedź brz­mi — oczy­wiś­cie, że moż­na, tylko po co? Mock­owanie daje nam przewidy­wal­ny wynik dzi­ała­nia konkret­nego ser­wisu, nieza­leżny od np. połączenia z inny­mi sys­tema­mi, danych w bazie danych czy innych okolicznoś­ci. Dzię­ki temu unikamy sytu­acji w której testy dają różne wyni­ki w zależnoś­ci od tego, kiedy, jak i na jakim urządze­niu zostaną urochomione.

Dru­ga zale­ta to możli­wość rekon­fig­u­racji — dodatkowa imple­men­tac­ja jakiegoś ser­wisu tylko na potrze­by tes­tu jest ogranic­zona jeśli chodzi o skra­jne przy­pad­ki. Moc­ki pozwala­ją nam zmieni­ać zachowanie mock­ów pomiędzy tes­ta­mi, czyniąc je wyjątkowo elasty­cznym i przy­dat­nym narzędziem do testów.

Zanim zaczniemy

Pobieranie bibliteki

Bib­liotekę Mock­i­to najłatwiej pobrać z cen­tral­nego repozy­to­ri­um Maven — intere­su­je nas arte­fakt org.mockito:mockito-all.

Źródła omawianych klas

Na potrze­by tego wpisu załóżmy, że mamy np. klasę Noti­fi­ca­tion­Ser­vice . Korzys­ta ona z imple­men­tacji 2 inter­fe­jsów — SMSSer­vice oraz EmailSer­vice, oraz udostęp­nia jed­ną metodę send­No­ti­fi­ca­tion (która w założe­niu powin­na wywołać metody obu tych inter­fe­jsów, jeśli podamy odpowied­nie dane). Nasze klasy wyglą­da­ją 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ć! Mock­i­to pozwala na dwa ‘try­by’ rozpoczę­cia pra­cy — deklaru­jąc odpowied­nią klasę wykonu­jącą testy (za pomocą adno­tacji @RunWith) oraz wywołu­jąc metodę init­Mocks np. w kon­struk­torze klasy. Gorą­co zale­camy korzys­tać z adno­tacji, ale przed­staw­imy obie te metody. Funkcjon­al­nie nie ma pomiędzy nimi różni­cy, różnią się jedynie czytel­noś­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 pode­jś­cia do tego, które zależą od sposobu, w jaki tworzysz klasy.

Mockowanie pól z adnotacją @Autowired

Meto­da ta sprawdzi się, jeśli wszys­tkie pola testowanej klasy mają adno­tację @Autowired, i chce­my korzys­tać tylko z mock­ów. Wtedy wystar­czy użyć adno­tacji @InjectMocks, jak na poniższym przykładzie:

@RunWith(MockitoJUnitRunner.class)
public class NotificationServiceTest {
    @Mock
    EmailService emailService;
    @Mock
    SMSService smsService;

    @InjectMocks
    NotificationServiceImpl notificationService;

}

Pomi­mo, że jest to wspier­any przez Mock­i­to sposób, zale­cane jest uży­wanie drugiej metody

Wstrzykiwanie za pomocą konstruktora

Dru­gi sposób to wstrzyknię­cie zależnoś­ci poprzez kon­struk­tor. Jest to prefer­owana meto­da (dla zain­tere­sowanych pole­cam 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 twor­zony przed każdym testem od nowa. To raczej zale­cana prak­ty­ka (dzię­ki temu elimin­u­je­my możli­wą ‘stanowość’ ser­wisu pomiędzy tes­ta­mi), nato­mi­ast jeśli samo tworze­nie ser­wisu jest czasochłonne, wyma­ga dużej iloś­ci pamię­ci lub też testowany obiekt nie może być w wielu instanc­jach, moż­na to zachowanie zmody­fikować wg potrzeb.

Konfiguracja mocków

Mając już poprawnie zainicjowane moc­ki może­my przys­tąpić do ich kon­fig­u­racji. Może­my to zro­bić w dwóch miejs­cach — albo w metodzie z adno­tacją @Before albo w konkret­nym teś­cie. Różni­ca jest taka, że meto­da z adno­tacją @Before spowodu­je, że wszys­tkie testy w tej klasie będą korzys­tały z tej kon­fig­u­racji, kon­fig­u­rac­ja w metodzie tes­tu­jacej oczy­wiś­cie będzie ‘widocz­na’ tylko w ramach tego jed­nego tes­tu. Moż­na jed­nak nad­pisy­wać kon­fig­u­rację — dlat­ego warto w metodzie z adno­tacją @Before skon­fig­urować ‘domyślne’ zachowanie mock­ów, i ew w zależnoś­ci od potrze­by mody­fikować je w poje­dynczych testach.

Co ważne — nie musimy kon­fig­urować wszys­t­kich metod moc­ka! Wystar­czy skon­fig­urować te, które będą wyko­rzysty­wane (pośred­nio oczy­wiś­cie) przez nasze testy.

Konfiguracja obiektu zwracanego z metody — statyczny obiekt

Najprost­szy sposób kon­fig­u­racji danej metody moc­ka to zwróce­nie staty­cznego obiek­tu dla każdego wywoła­nia tej metody. To oczy­wiś­cie nie zawsze dobra prak­ty­ka, ale bardziej rozbu­dowane możli­woś­ci omówimy za chwilę. Aby zwró­cić obiekt dla każdego wywoła­nia metody nieza­leżnie od argu­men­tów, uży­wamy 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 dowol­nego obiek­tu podanego jako argu­ment. O innych możli­woś­ci­ach w tym zakre­sie powiemy sobie za chwilę.

Powyższy kod moż­na przeczy­tać jako:

Kiedy wywołana zostanie meto­da sendE­mail moc­ka emailSer­vice, z dowol­ny­mi argu­men­ta­mi, zwróć obiekt message.

Metody nie zwracające wartości

Szczegól­nym przy­pad­kiem są metody, które nie zwraca­ją żad­nego obiek­tu, powyższa skład­nia oczy­wiś­cie nie zadzi­ała, ale ist­nieje sposób, aby to obe­jść (na potrze­by tego przykładu załóżmy, że meto­da sendE­mail nie zwraca niczego):

Mockito.doNothing().when(emailService).sendEmail(Matchers.anyObject(), Matchers.anyObject()));

Ta skład­nia może być uży­wana także do mock­owa­nia ‘stan­dar­d­owych’ metod, ale jest mniej czytel­na od omaw­ianych wyżej metod.

Konfiguracja parametrów metody

Mock­i­to pozwala nam kon­fig­urować zachowanie metody także w zależnoś­ci od tego, z jaki­mi argu­men­ta­mi 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 przeczy­tać jako:

Kiedy wywołana zostanie meto­da sendE­mail moc­ka emailSer­vice, z dowol­ny­mi argu­men­ta­mi, zwróć obiekt mes­sage, ale kiedy pier­wszy argu­ment jest równy (równy w sen­sie ‘equals’) “[email protected]”, zwróć obiekt adminMessage.

To bard­zo elasty­czne rozwiązanie, a ilość możli­wych sprawdzeń, jakie mamy dostęp­ne jest ogrom­na (pełną listę zna­jdzies w doku­men­tacji API klasy Match­ers). Z ważniejszych warto wspom­nieć o isA (klasa określonego typu), isNot­Null (argu­ment nie jest nullem), startsWith (ciąg znaków zaczy­na się od …).

To tylko jed­na opc­ja, jeśli chodzi o zmi­anę zachowa­nia w zależnoś­ci od argu­men­tów. Może­my także wykon­ać określony kod z uży­ciem wspom­ni­anych argu­men­tów, który wygeneru­je obiekt odpowiedzi — o tym powiemy sobie trochę dalej.

Konfiguracja obiektu zwracanego z wykorzystaniem argumentów wywołania metody

Poza zwracaniem staty­cznego obiek­tu, Mock­i­to pozwala nam także gen­erować obiekt zwracany na pod­staw­ie przekazy­wanych argu­men­tów. Jest to szczegól­nie przy­datne, jeśli mock­u­je­my inter­fe­jsy 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 przeczy­tać jako:

Kiedy wywołana zostanie meto­da sendE­mail moc­ka emailSer­vice, z dowol­ny­mi argu­men­ta­mi, zwróć obiekt EmailMes­sage tworząc go przekazu­jąc jako argu­men­ty kon­struk­to­ra argu­men­ty użyte do wywoła­nia metody.

Oczy­wiś­cie kod ten moż­na nieco uproś­cić uży­wa­jąc lamb­dy, ale o niej będzie dopiero w kole­jnych wpisach :)

Rzucanie wyjątków w mockach

Na potrze­by testów cza­sem chce­my, aby meto­da rzu­ciła wyjątek. Oczy­wiś­cie z Mock­i­to nie ma żad­nego prob­le­mu, jak na poniższym przykładzie:

RuntimeException exception = ... ;
Mockito.when(emailService.sendEmail(Matchers.anyObject(), Matchers.anyObject())).thenThrow(exception);

Powyższy kod moż­na przeczy­tać jako:

Kiedy wywołana zostanie meto­da sendE­mail moc­ka emailSer­vice, z dowol­ny­mi argu­men­ta­mi, rzuć wyjątek typu RuntimeException.

Weryfikacja informacji z mocka

Moc­ki mogą nam służyć nie tylko do ‘udawa­nia’ obiek­tów, ale także do wery­fikowa­nia — może­my np. sprawdz­ić, ile razy dana meto­da była uru­chomiona lub też z jaki­mi para­me­tra­mi została uru­chomiona. Dzię­ki temu może­my rozsz­erzyc testy o wery­fikację, czy komu­nikac­ja z zależnoś­ci­a­mi fak­ty­cznie występuje.

Weryfikacja ile razy dana metoda mocka zostala wywołana

Mock­i­to pozwala nam na prostą wery­fikację, ile razy meto­da została uży­ta. Wery­fikac­ja ta rządzi się podob­ny­mi prawa­mi, co kon­fig­u­rac­ja mock­ów — może­my określić warun­ki, jakie mają speł­ni­ać argu­men­ty, aby zlicze­nie nastąpiło. Mech­a­nizm ten omówiony jest szerzej we wczes­niejszych sekc­jach, tutaj ograniczymy się do prostego przykładu.

Mockito.verify(emailService, Mockito.times(1)).sendEmail(Matchers.anyObject(), Matchers.anyObject());

Powyższa lin­ij­ka sprawdzi, czy meto­da sendE­mail została wywołana z dowol­ny­mi argu­men­ta­mi dokład­nie raz.

Mockito.verify(emailService, Mockito.atLeast(1)).sendEmail(Matchers.startsWith("admin@"), Matchers.anyObject());

Ta lin­ij­ka z kolei wery­fiku­je, czy meto­da sendE­mail została wywołana z pier­wszym para­me­trem zaczy­na­ją­cym się od ‘admin@’ co najm­niej raz.

Weryfikacja argumentów, z jakimi metoda mocka została wywołana

W tes­tach może­my także wery­fikować, jakie argu­men­ty zostały użyte do wywoła­nia moc­ka. Dzię­ki temu możli­we jest sprawdze­nie, czy np. argu­ment został praw­idłowo przetwor­zony przed przekazaniem do moc­ka. W poniższym przykładzie sprawdza­my jeden z argu­men­tó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 lin­ij­ka sprawdzi, czy meto­da sendE­mail została wywołana z argu­mentem “[email protected]” przekazanym jako pier­wszy. Oczy­wiś­cie może­my także ‘przech­wyty­wać’ 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że­my także wery­fikować argu­men­ty, 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

Zna­jo­mość Mock­i­to wprawdzie rzad­ko kiedy jest wyma­gana na stanowisku Juniorskim, niem­niej jest to bard­zo przy­dat­na bib­liote­ka i jej zna­jo­mość z pewnoś­cią będzie plusem dla Ciebie w więk­szoś­ci firm.

Bib­liote­ka Mock­i­to dzię­ki elasty­cznoś­ci jest de-fac­to stan­dar­d­em jes­li chodzi o testy jed­nos­tkowe w pro­jek­tach i umiejętne korzys­tanie z niej zaoszczędzi Ci wiele cza­su i nerwów ;)