DevCezz

Programistyczny blog dla Ciebie

biblioteka mockito
Biblioteki Programowanie

Wykorzystaj Mockito w swoich testach

Implementując naszą aplikację często musimy komunikować się ze „światem zewnętrznym” czy to za pomocą plików, czy też przez sieć. Gdy już kod działa produkcyjnie to nie ma on najmniejszych problemów (załóżmy ten pozytywny scenariusz 😉), aby porozumiewać się z innymi systemami po HTTP. Co jednak w przypadku, gdy musimy napisać testy weryfikujące działanie naszej aplikacji? Nie będziemy przecież odpytywać za każdym razem zewnętrznych serwisów z kilku powodów:

  • Chcielibyśmy, aby praca odbywała się lokalnie np. podczas lotu samolotem, gdzie niekoniecznie musimy mieć połączenie z Internetem.
  • Odpytywanie innych klientów może wiązać się z kosztami (dostawca może pobierać opłaty za korzystanie z jego API).
  • Nie chcemy zmieniać danych produkcyjnych podczas tworzenia nowego rozwiązania (dane mogą przez to szybko stać się niespójne).
  • Łączenie się przez warstwę sieciową może być czasochłonne (zwłaszcza, gdy odbywa się to dla wszystkich dostępnych scenariuszy testowych).

Można by znaleźć jeszcze wiele innych czynników przemawiających za brakiem interakcji z różnymi usługami podczas testów. Gdyby tak można było wykorzystać jakieś rozwiązanie, które będzie nam szybko odpowiadać w taki sposób jaki sobie zażyczymy… Tutaj na pomoc przychodzi Mockito! Przynajmniej w świecie Javowym.

Czym właściwie jest Mockito? Jest to biblioteka, która pozwala nam tworzyć atrapy obiektów, czyli inaczej mówiąc „zaślepia” ich implementację. Programista ma dowolność w definiowaniu co dany obiekt ma robić bez zbędnego przechodzenia przez proces jego tworzenia. W tym momencie należy zwrócić uwagę na fakt, że Mockito możemy wykorzystywać na dwa sposoby.

Odwieczna walka pomiędzy Stub a Mock

Stub może nam służyć jako implementacja danego obiektu, która będzie zajmowała się tylko i wyłącznie zwracaniem danych. Nastąpi to w momencie, gdy jedna z jego metod zostanie wywołana. Przykładowo mamy obiekt klasy ProductRepository i chcielibyśmy, żeby w momencie wywołania jego metody findAnyProduct został zwrócony za każdym razem wybrany przez nas produkt. W tym momencie powinniśmy użyć stuba.

schemat działania stuba
Przykład działania stuba

Natomiast w przypadku, gdy chcielibyśmy zweryfikować ile razy, z jakimi parametrami oraz jakie metody zostały wywołane na danym obiekcie powinniśmy wykorzystać mocka. Oczywiście w przypadku, gdy dana metoda coś zwraca to i tak musimy nauczyć naszego mocka co dokładnie ma zwracać, aby nie dostać naszego ulubionego NullPointerException.

schemat działania mocka
Przykład działania mocka

Z tego właśnie powodu Mockito łączy obydwie koncepcje. Przy małym nakładzie pracy możemy nauczyć nasz obiekt co ma zwracać oraz sprawdzić jakie interakcje z nim zachodziły. Więc przejdźmy do sedna i sprawdźmy w jaki sposób możemy korzystać z tej jakże przydatnej biblioteki.

Przykłady zastosowania Mockito

Jak wygląda użycie w kodzie? Na początek musimy zaciągnąć bibliotekę Mockito z repozytorium Mavena. Aktualnie znajduje się ona w wersji 3.8.0 i na bieżąco jest rozwijana. Zacznijmy od zdefiniowania klasy, którą będziemy chcieli przetestować jednostkowo. Powiedzmy, że to będzie serwisu do obsługi produktów.

class ProductService {

    private final ProductRepository productRepository;

    ProductService(final ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    Result increasePriceOfAnyProduct(int amount) {
        Optional<Product> product = productRepository.findAnyProduct();

        return product.map(p -> {
            p.increasePrice(amount);
            productRepository.save(p);
            return Result.SUCCESS;
        }).orElse(Result.ERROR);
    }
}

Jedyną zależnością naszej klasy jest ProductRepository, który posiada metodę findAnyProduct. Zwraca nam on Optional<Product>. Jeżeli jest w nim produkt do zwiększamy mu cenę, zapisujemy i ogłaszamy sukces. Jeśli natomiast nie otrzymaliśmy żadnego produktu od naszego repozytorium to zwracamy błąd. To jak w tym przypadku może wyglądać klasa testowa z użyciem Mockito? Na początek musimy ustawić nasze zaślepki.

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
...

class ProductServiceTest {

    @Mock
    ProductRepository productRepository;

    @InjectMocks
    ProductService productService;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void should_return_success_when_product_has_been_returned() {
        ...
    }
}

Nad zależnościami do naszej testowanej klasy należy dodać adnotację @Mock, która tworzy zaślepkę dla ProductRepository. Następnie musimy te zależności wstrzyknąć. W tym celu korzystamy z adnotacji @InjectMocks nad klasą ProductService. Oczywiście musimy jeszcze poinstruować, aby przed każdy testem mocki zostały ustawione w klasie testowej. Robimy to przy pomocy MockitoAnnotations.openMocks(this). Takie przygotowania pozwalają nam na bezproblemowe przeprowadzenie testów jednostkowych. Mi osobiście ten zapis z adnotacjami się nie podoba, dlatego polecam drugie podejście.

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
...

import static org.mockito.Mockito.mock;
...

class ProductServiceTest {

    ProductRepository productRepository = mock(ProductRepository.class);

    ProductService productService;

    @BeforeEach
    void setUp() {
        productService = new ProductService(productRepository);
    }

    @Test
    void should_return_success_when_product_has_been_returned() {
        ...
    }
}

Teraz widzimy dokładnie ile mamy zależności niezbędnych do otrzymania ProductService. Jeżeli byłoby ich za dużo to na pewno wtedy to zauważymy. Inaczej ma się kwestia, gdy radośnie używamy adnotacji @Mock i nie widzimy momentu tworzenia obiektu testowanego. Nie mamy świadomości ile klocków potrzebujemy do jego inicjalizacji.

No dobrze to skoro już stworzyliśmy naszą zaślepkę to pora jakoś ją wykorzystać. Zacznijmy od testu, który pokaże nam w jaki sposób możemy stubować przy wykorzystaniu Mockito.

Stub w teście

import org.junit.jupiter.api.Test;
import java.util.Optional;
...

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
...

class ProductServiceTest {

    @Test
    void should_return_success_when_product_has_been_returned() {
        given(productRepository.findAnyProduct())
                .willReturn(Optional.of(new Product()));

        Result result = productService.increasePriceOfAnyProduct(100);

        assertThat(result).isEqualTo(Result.SUCCESS);
    }
}

Linijki 3 i 4 pokazują nam w jaki sposób możemy stworzyć stuba! Przekazując do metody statycznej BDDMockito.given naszą zależność, a dokładniej jej metodę, możemy nauczyć ją co ona ma zwracać. W ten sposób nie jesteśmy w żaden sposób zależni od jej implementacji, czyli jak można się domyślić, od połączenia z bazą danych. Może to być równie dobrze wywołanie zewnętrznego API, którego fizycznie nie chcemy używać. Jest to spowodowane tym, że testy jednostkowe muszą dawać nam natychmiastowy feedback, a wołanie usług zewnętrznych na pewno zabiera więcej czasu niż operacje na maszynie lokalnej.

Przy okazji zwrócę uwagę na metodę assertThat pochodzącą z AssertJ, która pozwala nam sprawdzenie rezultatów działania naszej metody. Naprawdę polecam się z nią zapoznać. Na pewno ma przyjemniejsze API niż JUnit.

Wracając do Mockito, pokazałem tutaj użycie metody given, ponieważ bardziej zbliża nas ona do konwencji given/when/then. Istnieje również możliwość wykorzystania innej metody z klasy Mockito.

when(productRepository.findAnyProduct()).thenReturn(Optional.of(new Product()));

Ten zapis również nam zadziała tak jak poprzedni jednak nie przemawia on do mnie z wcześniej wymienionego powodu.

Mock w teście

Przyszedł czas na weryfikację interakcji. Zapędzimy do tego mocka, dzięki któremu sprawdzimy czy zostanie wywołany zapis na bazie danych dokładnie jeden raz. Zapiszmy na tą okoliczność następny test.

import org.junit.jupiter.api.Test;
import java.util.Optional;
...

import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
...

class ProductServiceTest {
    
    @Test
    void should_save_product_with_increased_price() {
        Product found = new Product();
        given(productRepository.findAnyProduct())
                .willReturn(Optional.of(new Product()));

        productService.increasePriceOfAnyProduct(100);

        verify(productRepository).save(found);
    }
}

Pojawiła nam się nowa konstrukcja verify. Dzięki niej możemy sprawdzić czy metoda save została wywołana dokładnie raz z wybranym parametrem. Jest to dokładnie to o czym wspominałem wcześniej, weryfikujemy interakcję z naszymi zależnościami. Warto podkreślić fakt, że możemy również zapisać naszą linijkę w następujący sposób.

verify(productRepository, times(1)).save(found);

Jednak metoda verify domyślnie jest wywoływana właśnie z parametrem times(1), ale jeżeli ktoś chce to ma taką możliwość, aby podkreślić ten fakt.

Według mnie ważne jest, aby nie mieszać ze sobą asercji i sprawdzenia interakcji w jednym teście. Dokładniej chodzi o assertThat oraz verify. W ten sposób możemy uniknąć sporego bałaganu w testach jednostkowych kosztem tak naprawdę kilku milisekund.

Podsumowanie

Na pewno tak jak wszystko, Mockito też trzeba używać z rozwagą. Powinniśmy uważać na to, aby nie mockować dosłownie wszystkiego albo, aby w mocku nie zwracać mocka. Łatwo popaść w skrajność, w której zamiast testowania funkcjonalności będziemy sprawdzać czy biblioteka do mocków poprawnie działa. Naprawdę przestrzegam przed tym. Na pewno uważajcie też na mockowanie metod statycznych. Jeżeli kiedykolwiek zajdzie taka potrzeba to przemyślcie kilka razy czy Wasz kod czasem nie potrzebuje zmian. Sam na początku się na to łapałem i próbowałem korzystać z PowerMocka na nieszczęsnym LocalDate.now(). Specjalnie nie podaję linka, aby nikogo nie kusiło.

Mam nadzieję, że ten artykuł dostarczył Ci pogląd na bibliotekę Mockito. Jest ona naprawdę potrzebna do testów jednostkowych, aby można było szybko sprawdzić czy nasz kod działa zgodnie z założeniami. Ja natomiast zachęcam Cię do dodania komentarza pod tym artykułem. Chciałbym się dowiedzieć czy to co robię jest dla Ciebie w jakiś sposób pomocne. Zajrzyj również na mój artykuł, który przedstawia 6 powodów, dla których warto pisać testy.

A tym czasem powodzenia i cześć!

Podziel się tym z innymi!