Java Developer? Przejdź na wyższy poziom wiedzy 🔥💪  Sprawdź

Team Leader? Podnieś efektywność swojego zespołu 👌 Sprawdź

Mockito w pigułce! Poznaj dobre praktyki i przeczytaj czego nie mockować

utworzone przez Java, Testowanie

Mockito to na ten moment najczęściej pobierana biblioteka z javy w Maven Central (nie licząc frameworka JUnit). Jej zadaniem jest ułatwienie testowania kodu, w izolacji od jego zależności. Kluczowe funkcjonalności Mockito to:

  • Zmiana zachowania obiektów (zależności) – zwracanie wartości, zgłaszanie wyjątków itp.
  • Weryfikacja interakcji (czy metoda została wywołana) – czy wystąpiły interakcje, ile razy i z jakimi parametrami.

Poniżej zebrałem najważniejsze informacje. Nie przedłużając, startujemy.

Z tego artykułu dowiesz się

  • Czym jest Mock i jak działa
  • Czym jest Spy
  • Verify, InOrder
  • Argument matchers
  • Argument captors
  • Answers
  • Nice mocks
  • Czego nie mockować
Logo Mockito
Strona projektu: https://mockito.org/

Piguła wiedzy o najlepszych praktykach testowania w Java

Pobierz za darmo książkę 100 stron o technikach testowania w Java

Zdefiniowanie zależności

Aby dodać Mockito do projektu, definiujemy zależność (w zakresie test) do mockito-core. Przykładowa deklaracja dla Maven:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.4.6</version>
    <scope>test</scope>
</dependency>

Aby użyć Mockito z biblioteką JUnit 5, definiujemy dodatkowo integrację:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>2.23.0</version>
    <scope>test</scope>
</dependency>

Mock

Mock (inaczej dubler) to obiekt, którego celem jest zastąpienie oryginalnego obiektu w celu określenia jego zachowania. Całkowicie zastępuje prawdziwy obiekt, ale zachowuje jego publiczne API (metody).

Naturalne jest, że w projekcie, większy kod dzielony jest na mniejsze fragmenty. Wtedy większe funkcjonalności komponowane są z mniejszych. Do tego celu służy dependency injection (tzw. wstrzykiwanie zależności). Kiedy testujemy fragment kodu, który ma zależność do innego fragmentu kodu (klasa używa innej do realizacji swojej funkcjonalności), wówczas mock rozwiązuje problem posiadania „wstrzykniętej” implementacji, której wynik ma być zgodny z założeniami przeprowadzenia testu (warunki wstępne). Po prostu nie trzeba pisać klasy zamiennej, która zwróci oczekiwaną wartość na potrzeby przetestowania.

Załóżmy, że w systemie istnieje klasa DistanceService, która określa odległość pomiędzy adresami (np. doręczenia i wysyłki). Aby zmienić jej zachowanie (zastubować je), na potrzeby testu używając mockito piszemy:

when(distanceService.getDistanceBetween(warehouseAddress, shippingAddress))
        .thenReturn(10);

Lub inaczej, używając stylu BDD (Behaviour-Driven Development) i nazewnictwa bliższego językowi naturalnemu, czyli: Given/When/Then dzięki BDDMockito:

given(distanceService.getDistanceBetween(warehouseAddress, shippingAddress))
        .willReturn(10);

Jak widać, nie musimy implementować zaślepki, która zwróci oczekiwaną wartość (stub’a) na potrzeby naszego testu.

Mocka możemy stworzyć ręcznie:

import static org.mockito.Mockito.mock;

DistanceService distanceService = mock(DistanceService.class);

Lub posłużyć się adnotacją w teście:

@Mock Address shippingAddress;

Czym tak na prawdę jest stworzona instancja Mocka?

Można zadać pytanie, skoro Mock to zwyczajny, zainstancjonowany obiekt klasy (typu), ale bez implementacji, za to z wyspecyfikowanym zachowaniem – jak to właściwie działa?

Gdy postawimy breakpoint w teście, zauważymy:

Mockito object instance

Stworzony obiekt to dynamiczne proxy przechwytujące wywołania metod i zwracające wyniki zgodnie z oczekiwaniami oraz obserwujące interakcje. Wskazuje na to nazwa DistanceService$MockitoMock$identyfikator.

Kiedy używać mock`a:

  • W celu izolacji zależności w kodzie i wyspecyfikowaniu ich zachowania.
  • W celu stworzenia zależności, aby sprawdzić interakcje testowanej klasy z nimi.

Kiedy nie używać mock`a:

  • Mocków nie używamy do danych testowych oraz Value objectów. Do tego służą Matki Obiektów i Test Buildery.
  • Mockowanie kodu bibliotek. Ich zachowanie może się zmieniać i nie zostaniemy o tym poinformowani.

Wiele wartości

Jedną definicją mocka można zadeklarować zwrócenie kolejnych wartości przy kolejnych wywołaniach kodu. Dla przykładu – kolejne wywołania zwrócą wartość 10, następnie 20, a potem 30:

when(distanceService.getDistanceBetween(warehouseAddress, shippingAddress))
        .thenReturn(10, 20, 30);
// lub
given(distanceService.getDistanceBetween(warehouseAddress, shippingAddress))
        .willReturn(10, 20, 30);

Zgłaszanie wyjątków

Wynikiem działania mocka może być również zgłoszenie wyjątku:

when(distanceService.getDistanceBetween(warehouseAddress, shippingAddress))
        .thenThrow(new IllegalArgumentException("Message"));
// lub
given(distanceService.getDistanceBetween(warehouseAddress, shippingAddress))
        .willThrow(new IllegalArgumentException("Message"));

Dla metod zwracających typ void:

doThrow(new IllegalArgumentException("Message"))
        .when(distanceService).someMethod();

Psst… Interesujący artykuł?

Jeżeli podoba Ci się ten artykuł i chcesz takich więcej – dołącz do newslettera. Nie ominą Cię materiały tego typu.

.

Spy

Spy to inaczej szpieg. Jest podobny do Mock’a, natomiast, zamiast zastępować działanie obiektu, podgląda jego wywołania, nie zmieniając przy tym zachowania metod.

Spy może również częściowo zmieniać zachowanie podglądanego obiektu – zachowuje się wtedy jak Mock. Specyfikowanie interakcji jest takie samo jak dla Mocków.

List<String> list = spy(new ArrayList<String>());

list.add("element 1");
list.add("element 2");

assertThat(list).containsOnly("element 1", "element 2");

when(list.size()).thenReturn(100);
assertThat(list.size()).isEqualTo(100);

verify(list).add("element 1");
verify(list).add("element 2");

Utworzyłem zwykłą listę ArrayList i udekorowałem ją w spy(). Zmienna list tak naprawdę zawiera proxy do oryginalnie utworzonego obiektu. Operacje dodawania działają jak zwykle, ale zmieniłem zachowanie metody size(), która zwraca wartość 100 (zamiast tego, że zwróciłaby 2). Dodatkowo na końcu mogłem zweryfikować metodą verify() czy zostały wykonane metody add().

Błędem, z którym możesz się spotkać, jest weryfikacja nie tego obiektu, co trzeba. Pamiętaj, że następujący zapis:

List<String> originalList = new ArrayList<>();
List<String> list = spy(originalList);

Spowoduje, że szpiegowanym obiektem będzie tylko list, a originalList to oryginalna implementacja. Wszelkiego rodzaju metody takie jak given()/verify() zadziałają tylko na obiekcie originalList. Pamiętaj o tym, gdy będziesz przekazywać szpiega do implementacji, którą testujesz. Gdy uruchomisz debuger zauważysz, że implementacja originalList nie jest szpiegiem – jest nim natomiast (ArrayList@2516)(ArrayList$MockitoMock$…):

Mockito spy instance

Verify

Verify służy do weryfikowania wywołania metod na Mock/Spy. Po co? Załóżmy, że nasza klasa implementuje pewien proces, np. zamiany koszyka zakupowego (który modyfikuje klient) w zamówieniu (obsługiwane przez dział logistyki):

@RequiredArgsConstructor
public class CreateOrderService {

    private final BasketRepository basketRepository;
    private final OrderService orderService;

    public String createOrder(UUID basketId) {
        Basket basket = basketRepository.getById(basketId)
             .orElseThrow(new IllegalArgumentException("Basket not found"));

        return orderService.createOrder(basket);
    }
}

Z punktu widzenia klasy CreateOrderService, koszyk odnajdywany jest po ID, a następnie przekazywany jest do klasy OrderService – w celu złożenia zamówienia w systemie i zwrócenia jego ID. Zakładamy, że OrderService wie, w jaki sposób stworzyć zamówienie – testujemy wówczas funkcjonalność polegającą na poinformowaniu go, że zamówienie nastąpiło.

given(basketRepository.getById(basket.getBasketId()))
    .willReturn(basket);

createOrderService.createOrder(basket.getBasketId());

verify(orderService).createOrder(basket);

Inny zapis tej samej weryfikacji w BDD, bliższy językowi naturalnemu:

then(orderService).should().createOrder(basket);

Powyższy zapis wygląda jak fragment wyciągnięty wprost ze specyfikacji, prawda? Stosuj go tam, gdzie ma to sens. Taką formę czyta się szczególnie dobrze, gdy nazwa obiektu to rzeczownik (podmiot), a nazwy metod to orzeczenia w formie imperatywu + przydawka.

Przykładowy błąd asercji weryfikacji:

Wanted but not invoked:
orderService.createOrder(…);
-> at CreateOrderServiceTest.createsOrderForBasket(CreateOrderServiceTest.java:45)
Actually, there were zero interactions with this mock.

Krotność verify

Można zweryfikować konkretną liczbę wywołań:

verify(orderService, times(2)).createOrder(any(Basket.class));
// lub
then(orderService).should(times(2)).createOrder(any(Basket.class));

Przykładowy błąd sprawdzenia kilkukrotnego wykonania:

org.mockito.exceptions.verification.TooFewActualInvocations:
orderService.createOrder(…);
Wanted 2 times:
-> at CreateOrderServiceTest.createsOrderForBasket(CreateOrderServiceTest.java:45)
But was 1 time:
-> at CreateOrderService.createOrder(CreateOrderService.java:21)

Lub określić co najwyżej, co najmniej:

verify(orderService, atMost(2)).createOrder(any(Basket.class));
// lub
then(orderService).should(atMost(2)).createOrder(any(Basket.class));

verify(orderService, atLeast(2)).createOrder(any(Basket.class));
// lub
then(orderService).should(atLeast(2)).createOrder(any(Basket.class));

Gdy zależy Ci na tym, żeby sprawdzić, że nie doszło do interakcji z daną metodą:

verify(orderService, never()).createOrder(any(Basket.class));
// lub
then(orderService).should(never()).createOrder(any(Basket.class));

W praktyce częściej jednak sprawdza się, że nie zaszła żadna interakcja:

verifyNoInteractions(orderService);
// lub
then(orderService).shouldHaveNoInteractions();

Lub żadna inna oprócz wymienionych w teście:

verifyNoMoreInteractions(orderService);
// lub
then(orderService).shouldHaveNoMoreInteractions();

Weryfikowanie kolejności wywołań

Może nam zależeć też na sprawdzeniu, czy interakcje wystąpiły w konkretnej kolejności. Wtedy wykorzystujemy InOrder. Działa on zarówno dla jednego jak i dla wielu mocków:

@Test
void inOrderTest() {
    List<String> list = spy(new ArrayList<String>());
    List<String> list2 = spy(new ArrayList<String>());

    list.add("element 1");
    list2.add("element 1");
    list.add("element 2");
    list2.add("element 2");

    InOrder then = inOrder(list, list2);
    then.verify(list).add("element 1");
    then.verify(list2).add("element 1");
    then.verify(list).add("element 2");
    then.verify(list2).add("element 2");
}

Powyższe informacje podsumowuję na infografice:

Mockito verify

Psst… Interesujący artykuł?

Jeżeli podoba Ci się ten artykuł i chcesz takich więcej – dołącz do newslettera. Nie ominą Cię materiały tego typu.

.

Argument matchers

Sterując zachowaniem za pomocą when()/given() – możesz je wysterować tylko dla wywołań dotyczących konkretnych argumentów, to samo, weryfikując wywołania metod poprzez verify(). Aby wyrazić warunki dla argumentów, należy skorzystać z Argument Matchers.

Domyślnie podając pewien obiekt jako argument – Mockito przyjmuje, że musi być to argument równy (equals) podanemu.

Możemy natomiast określić np. dowolny argument danego typu (matcher typu any()):

given(orderService.orderFor(any(Basket.class))).willReturn(...);

Przykład z weryfikacją:

verify(orderService, never()).createOrder(any(Basket.class));

Inne matchery:

  • eq() – obiekt równy podanemu,
  • refEq() – obiekt równy podanemu, porównując jego atrybuty refleksją (przydatne, gdy obiekty nie implementują metody equals w poprawny sposób),
  • any(Class) – obiekt określonego typu,
  • isA(Class) – obiekt implementujący określony typ,
  • anyInt(), anyLong()… – dowolna wartość dla typów prostych,
  • contains() / matches() / startsWith() – operacje na napisach,
  • argThat(ArgumentMatcher<T>) – własna implementacja ArgumentMatcher<T> dla typu T

Pamiętaj jednak, że nie można mieszać ze sobą argumentów bez matcherów oraz z zastosowanymi matcherami, na przykład:

basketRepository.findBy(any(String.class), "str2");

Otrzymasz błąd:

org.mockito.exceptions.misusing.InvalidUseOfMatchersException:
Invalid use of argument matchers!
2 matchers expected, 1 recorded:
-> at SampleTest.argumentMatcherTest(SampleTest.java:52)

This exception may occur if matchers are combined with raw values:
//incorrect:
someMethod(anyObject(), „raw String”);
When using matchers, all arguments have to be provided by matchers.
For example:
//correct:
someMethod(anyObject(), eq(„String by matcher”));

For more info see javadoc for Matchers class.

Jeżeli potrzebujesz użyć przynajmniej jednego matchera, musisz zamienić wszystkie argumenty na matchery:

basketRepository.findBy(any(String.class), eq("str2"));

Unikaj natomiast używania samych matcherów typu eq(), można po prostu wkleić augmenty wprost do wywołania.

Argument captors

Argument Captor to mechanizm przechwytywania argumentów z wywołania obiektu podczas weryfikacji. Oprócz samego wywołania metody można sprawdzić przechwycony argument.

Przydatne, gdy chcemy sprawdzić interakcję, której argument jest tworzony wewnątrz implementacji (nie ma do niego dostępu w teście) i chcemy sprawdzić poprawność tego obiektu.

  1. Definiujemy ArgumentCaptor:
  2. Używamy argument Captor w wywołaniu – w taki sam sposób jak Argument Matcher.
  3. ArgumentCaptor zawiera obiekt (lub wiele) zebranych podczas wywoływania metody.
@Captor
ArgumentCaptor<Basket> basketArgumentCaptor;

// ...

@Test
void usesBasketToCreateAnOrder() {
    // ...
    given(orderService.createOrder(basketArgumentCaptor.capture()))
       .willReturn(...);

    // when
    createOrderService.createOrder(basket.getBasketId());

    // then
    assertThat(basketArgumentCaptor.getValue())
            // ... własne asercje
}

Problem można ominąć, rozdzielając implementację tworzenia obiektu do innej klasy (klasy factory) i jej wstrzyknięcie – wtedy mamy dostęp do stworzonej klasy. Natomiast w przypadku wysokiej hermetyzacji kodu lub interakcji z bibliotekami – z pomocą przychodzi Argument Captor.

Psst… Interesujący artykuł?

Jeżeli podoba Ci się ten artykuł i chcesz takich więcej – dołącz do newslettera. Nie ominą Cię materiały tego typu.

.

Answers

Możesz spotkać sytuację, że zachowanie mocka będzie zależało od przekazanego argumentu. Wtedy odpowiedź mocka będzie niejako „dynamiczna”. W tym celu korzystamy z mechanizmu Answers, czyli odpowiedzi.

given(orderService.createOrder(any(Basket.class))).willAnswer(new Answer<String>() {
    @Override
    public String answer(InvocationOnMock invocation) throws Throwable {
        return ...;
    }
});

Nice mocks

Gdy dochodzi do sytuacji, kiedy w pewnym zakresie zdefiniujemy działanie dla mocka, ale kod produkcyjny używa obiektu w zakresie, którego przypadkowo nie zdefiniowaliśmy – Mockito stara się sprawić, aby kod działał w miarę poprawnie. Mam tutaj na myśli:

  • Wywołania niezaimplementowanych metod nie powodują błędu, po prostu nic się nie dzieje.
  • Zwracane są wartości „puste”, dla liczb 0, dla kolekcji – puste kolekcje (zamiast null) a dla nieznanych typów – null.

Czego nie mockować

Mockito to potężne narzędzie, którego łatwo nadużyć. Warto zaznaczyć, czego nie należy mockować:

Nie mockować za dużo

Mocka powinieneś używać świadomie:

  • Do izolacji testowanej funkcjonalności.
  • W celu weryfikacji procesu (verify na wywoływanych metodach).

Niemniej jednak warto, jest mieć test całościowy, np. testy komponentowe (poczytaj o Piramidzie Testów), które sprawdzą poskładany ze sobą kod – koniecznie z produkcyjną konfiguracją beanów (poczytaj o 100% Code Coverage i płonącej produkcji).

Nieużywanych obiektów

Nie mockuj obiektów, które nie są użyte w teście. To nadmiarowe. Mockito od pewnego czasu przypomina o takim zachowaniu:

org.mockito.exceptions.misusing.UnnecessaryStubbingException:
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
1. -> at pl.softwareskill.example.mockito.SampleTest.argumentMatcherTest(SampleTest.java:56)
Please remove unnecessary stubbings or use ‚lenient’ strictness. More info: javadoc for UnnecessaryStubbingException class.

Zagnieżdżonych struktur danych modelu nad którym panujesz

Jeżeli w kodzie widzisz, że jest drobina mockowania:

given(obj1.getObj2()).willReturn(obj2);
given(obj2.getValue()).willReturn(...);

lub tzw. Deep Stub:

Foo mock = mock(Foo.class, RETURNS_DEEP_STUBS);
when(mock.getBar().getName()).thenReturn("deep");

wiedz, że nie jest to najlepszy fragment kodu. Dużo lepiej użyć danych testowych: Matek Obiektów lub Test Builderów.

Metod statycznych

Mockowanie metod statycznych to zła praktyka. Podobnie jak trzymanie globalnego stanu w aplikacji. Metody statyczne nie powinny mieć efektów ubocznych (w globalnym stanie). Tym samym, powinny zwracać deterministyczne wyniki. Mając to na uwadze – daj im się po prostu wykonać.

Mockowanie statycznych metod to oznaka niewłaściwego designu aplikacji.

Czasami nie masz wyjścia, bo statyczna metoda przechowuje stan. Ale zanim to zrobisz, postaraj się zrobić tę funkcjonalność jako niestatyczną i wstrzyknij ją jako zależność.

Jeżeli naprawdę nie masz wyjścia (jest to funkcja biblioteczna lub część starego kodu), Mockito potrafi stubować również statyczne metody.

Zakończenie

Mockito to potężne narzędzie ułatwiające pracę z kodem, który dzięki Dependency Injection i kompozycji jest podzielony granuralnie, a testowane funkcjonalności mogą być w izolacji od siebie.

Ważne jest jednak, aby używać go świadomie.

Jeżeli ten artykuł uważasz za wartościowy, podziel się nim w zespole, na Social Media, chatach, itd.

Piguła wiedzy o najlepszych praktykach testowania w Java

Pobierz za darmo książkę 100 stron o technikach testowania w Java

Podoba Ci się ten artykuł? Weź więcej.

Jeżeli uważasz ten materiał za wartościowy i chcesz więcej treści tego typu – nie przegap ich i otrzymuj je prosto na swoją skrzynkę. Nawiążmy kontakt.

.

Gdybyś potrzebował jeszcze więcej:

Jesteś Java Developerem?

Przejdź na wyższy poziom wiedzy 
„Droga do Seniora” 🔥💪

Jesteś Team Leaderem? Masz zespół?

Podnieś efektywność i wiedzę swojego zespołu 👌

Obraz: Ludzie zdjęcie utworzone przez pvproductions – pl.freepik.com

Dyskusja