Już dostępny

Program Szkoleniowy Java Developer dostępny 🔥💪 tylko TERAZ za 1299 zł  Sprawdź szczegóły i agendę

Zakres

Monitoring • Apache Kafka • Clean Code Testowanie • Hibernate • Systemy kolejkowe Sprawdź szczegóły i agendę

Zakres

14 modułów  /  ponad 40h nagrań  /  230 lekcji  /  dożywotni dostęp  /  Sprawdź szczegóły i agendę

Mockować czy nie? 🤔 Czym jest Unit w unit testach? Dwie szkoły pisania testów

utworzone przez 20 września 2021Java, Testowanie

[Szybkie info]: Startujemy z IV edycją Programu Szkoleniowego Java Developera 🚀. To MEGA piguła wiedzy o Java 🔥💪

  • 14 tygodniowy program szkoleniowy online,
  • 230 lekcji w formie video (40 godzin materiału)
  • z dożywotnim dostępem
  • Case Studies, masz dostęp do kodu i obrazów Dockerowych
  • zamknięta grupa mentorzy + uczestnicy i webinary na żywo

W agendzie znajdziesz: Mikroserwisy, Systemy kolejnowe, Apache Kafka, Caching, Hibernate/MyBatis/Spring Data, techniki efektywnych Testów kodu, Clean Code i Maven.

Tylko teraz dołączysz z 50% rabatem to 2699 zł 1299 zł (+VAT). I nigdy już nie będzie taniej. Poniżej dowiesz się więcej:

Zobacz więcej

A teraz przechodzimy do artykułu:

Biblioteka Mockito umożliwia w łatwy sposób stworzenie „zaślepek”/”dublerów”, które zastępują prawdziwe obiekty naśladując ich zachowanie. Podczas testowania Mocki pomagają nam zredukować zależności dla sprawdzanej implementacji, aby skoncentrować się tylko i wyłącznie na testowanym fragmencie kodu, a nie jego zależnościach.

Mockowanie pozwala na ustaleniu zwracanych rezultatów oraz weryfikację interakcji na obiektach. Zakładamy wtedy, że to co mockujemy działa zgodnie z założeniami – zwraca odpowiednie dane, albo wywołane wykona oczekiwaną operację.

Możliwości biblioteki Mockito opisaliśmy w artykule „Mockito w pigułce”.

Być może uczestniczyłeś w dyskusjach – co mockować, a czego nie. Albo czym jest unit w unit testach?

Problem z nadmiernym zaślepianiem jest taki, że istnieje założenie, że dana implementacja działa zgodnie z założeniami zdefiniowanymi w teście, a tak na prawdę albo tak nie jest (bo brakuje testów), albo założenia wstępne są błędne (pomyłka). Albo, że testowany kod z zamockowanymi zależnościami może być w rzeczywistości inaczej skonfigurowany w testach, a inaczej w kodzie produkcyjnym.

Z tego artykułu dowiesz się:

  • Żeby coś porządnie przetestować to mockować, czy nie?
  • Jakie są problemy z mockowaniem?
  • Kiedy mockować, a kiedy nie?
  • Co daje nam piramida testów w kontekście mockowania?

W tym wpisie podejdę do tematu z wielu stron.

Przykład

Załóżmy że masz do przetestowania funkcjonalność tworzenia nowego zamówienia na podstawie koszyka zakupowego. To moment, kiedy użytkownik zamienia swoje produkty w koszyku i podejmuje decyzję zakupową, dlatego powstaje zamówienie.

Istnieją dwie reguły biznesowe:

  1. Nie można stworzyć zamówienia, jeżeli koszyk jest pusty.
  2. Nie można stworzyć zamówienia, kiedy jakikolwiek produkt dodany do koszyka przestał być dostępny.

Za złożenie zamówienia odpowiada klasa CreateOrderService, która wyciąga odpowiedni koszyk po UUID z repozytorium, sprawdza reguły biznesowe oraz powiadamia zewnętrzny OrderService, aby na podstawie produktów powstało zamówienie:

@RequiredArgsConstructor
public class CreateOrderService {

    private final BasketRepository basketRepository;
    private final OrderService orderService;
    private final OrderCreationPreconditions orderCreationPreconditions;

    public String createOrder(UUID basketId) {
        Basket basket = basketRepository.getById(basketId)
                .orElseThrow(this::onBasketNotFound);

        checkBasket(basket);

        return orderService.createOrder(basket);
    }

    private RuntimeException onBasketNotFound() {
        return new IllegalArgumentException("Basket not found.");
    }

    private void checkBasket(Basket basket) {
        if (orderCreationPreconditions.emptyBasket(basket)) {
            throw new IllegalStateException("Cannot create order from empty basket.");
        }
        if (!orderCreationPreconditions.allProductsAreAvailable(basket)) {
            throw new IllegalStateException("Some product is not available.");
        }
    }
}

Reguły biznesowe sprawdzane są w metodzie checkBasket. Regułę #1 można sprawdzić na samym obiekcie Basket, a regułę #2 przeglądając widok produktów. Dajmy na to, że produkty mają flagę available, która jest synchronizowana zdarzeniami z mikrousługi odpowiedzialnej za magazyn.

Na potrzeby tego przykładu wyodrębniłem je do pakietowej klasy (package-private) OrderCreationPreconditions.

class OrderCreationPreconditions {

    boolean emptyBasket(Basket basket) {
        return basket.getInsertedProducts().isEmpty();
    }

    boolean allProductsAreAvailable(Basket basket) {
        return basket.getInsertedProducts()
                .stream()
                .allMatch(Product::isAvailable);
    }
}

Nie jest teraz istotne to, czy testujemy z wykorzystaniem techniki TDD, czy „po napisaniu kodu”. Skupmy się na tym, w jaki sposób w obu tych technikach moglibyśmy podejść do przetestowania funkcjonalności.

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.

.

Czym jest unit w unit testach?

Aby przetestować implementację potrzebujemy zdefiniować, czym jest unit w unit testach.

Moglibyśmy popatrzeć na testy że unitem jest klasa + jej prywatne implementacje. Wtedy wyobrażalibyśmy sobie unit tak:

Z drugiej strony moglibyśmy uznać, że unitem jest pakiet i publiczne API w pakiecie. W testach nie są widoczne bloki budulcowe, z jakich zbudowany jest pakiet, tylko publiczne API. Wtedy wyobrażalibyśmy sobie unit tak:

Pytanie sprowadza się później do tego, co wstrzyknąć do klasy Service z package2 (różowy blok):

  • Zależność z package1 oraz klasę Decision z tego samego pakietu package2.
  • Czy tylko zależność z package1, ale prywatnej klasy Decision z tego samego package2 już nie, bo jest to niepubliczny detal implementacyjny pakietu package2.

Nie oceniaj na razie obu podejść.

Tu płynnie chcę przejść do dwóch szkół testowania.

Dwie szkoły testowania

Podczas popularyzowania techniki TDD powstały dwa obozy:

  • Szkoła Detroit (inaczej amerykańska, Classicist, state based testing, Black-box testing), popularyzowana przez Uncle Bob’a, Kent Beck’a.
  • Szkoła Londyńska (inaczej Mockist, Interaction testing, white-box testing), popularyzowana przez Steve Freeman’a, czy Nat Pryce’a.

Obie te szkoły uważają, że technika TDD to efektywne narzędzie, ale używają go w inny sposób.

W podejściu Szkoły Detroit zaczynamy pisać testy, a design i podział wewnętrzny jest naturalną pochodną. W tym podejściu nie mockujemy wewnętrznych zależności i testujemy to, co widać „z zewnątrz”. Wewnętrzne podziały i interakcje nie mają znaczenia, a mockujemy tylko zewnętrzne zależności do naszej funkcjonalności.

Z kolei szkoła Londyńska polega na mockowaniu zależności przy przyjęciu, że unitem jest klasa. Każda interakcja z sąsiednią klasą jest zamockowana, lub wywołanie jej jest weryfikowane. Idea jaka stoi za tym podejściem jest taka, że jeżeli każda klasa jest osobno przetestowana, można ją zamockować, bo wynik jej działania został przetestowany gdzieś indziej, a w naszym teście wykorzystujemy ten ustalony wynik.

Podsumowując:

Szkoła Amerykańska

  • Unit = pakiet (funkcjonalność)
  • Co mockujemy: zależności modułu
  • Nastawione na testowanie zachowania całego modułu. Trzeba wcześniej związać ze sobą klasy.
  • Nie skupia się na implementacji, skupia się na wyniku. Zmiana implementacji nie powoduje zmiany testów.
  • Jedna zmiana w klasie wewnątrz powoduje błąd w wielu testach

Szkoła Londyńska

  • Unit = klasa
  • Co mockujemy: zależności klasy
  • Nastawione na testowanie dokładnej implementacji. Zmiana implementacji = zmiana testów.
  • Jeżeli jakaś metoda jest przetetsowana w jednym miejscu, użyjmy jej w
    innym – i zweryfikujmy interackje (nie wynik)
  • Izolacja błędu: błąd w jednej klasie powoduje błąd jednego testu

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.

.

Przykłady testów

Idąc tymi dwoma szkołami, nasze testy mogłyby wyglądać tak:

Według szkoły Amerykańskiej (klasycznej):

@BeforeEach
void setup() {
	createOrderService = new CreateOrderService(basketRepository, orderService, new OrderCreationPreconditions());
}

@Test
@DisplayName("Does not create order for empty basket")
void doesNotCreateOrderForEmptyBasket() {
	// given
	Basket basket = givenBasketWithoutProducts();
	given(basketRepository.getById(basket.getBasketId())).willReturn(Optional.of(basket));

	// when
	assertThatCode(() -> createOrderService.createOrder(basket.getBasketId()))
			.isInstanceOf(IllegalStateException.class)
			.hasMessage("Cannot create order from empty basket.");

	// then
	then(orderService)
			.should(never())
			.createOrder(basket);
}

I test tej samej funkcjonalności wg szkoły Londyńskiej (mockistów):

@Mock
BasketRepository basketRepository;
@Mock
OrderService orderService;
@Mock
OrderCreationPreconditions orderCreationPreconditions;
@InjectMocks
CreateOrderService createOrderService;

@Test
@DisplayName("Does not create order for empty basket")
void doesNotCreateOrderForEmptyBasket() {
	// given
	Basket basket = givenBasketWithoutProducts();
	given(basketRepository.getById(basket.getBasketId())).willReturn(Optional.of(basket));

	given(orderCreationPreconditions.emptyBasket(basket)).willReturn(true);

	// when
	assertThatCode(() -> createOrderService.createOrder(basket.getBasketId()))
			.isInstanceOf(IllegalStateException.class)
			.hasMessage("Cannot create order from empty basket.");

	// then
	then(orderService)
			.should(never())
			.createOrder(basket);
}

Piramida testów

Jak widzisz – oba podejścia mają swoje wady i zalety. W szkole amerykańskiej można dowolnie komponować strukturę wewnętrzną pakietów. Ale testowanie skrajnych przypadków jest na wyższym poziomie, a jeden błąd w podklasie powoduje eksplozję wielu testów – brak izolacji małych fragmentów.

Z jednej strony szkoła mockistów pozwala dobrze wyizolować problemy, ale za to bazujemy na interakcjach i w rzeczywistości kod może nie działać ze wzlędu na błędne założenia w testach. Możemy mieć zielone testy i płonącą produkcję. Także refaktorzyacje są cięższe – inna kompozycja obiektów zmienia mockowanie.

Dobrym kompromisem jest stosowanie testów komponentowych (component tests) o jeden poziom wyżej w piramidzie testów (o piramidzie testów przeczytasz tutaj). Są to testy, w których klasy są już powiązane (np. konfiguracja kontekstu Spring), mockowana jest tylko infrastruktura (I/O), a funkcjonalność można przetestować całościowo. Trochę jak w podejściu szkoły Londyńskiej. Testy działają nieco wolniej, ale nie muszą być bardzo szczegółowe – kilka przypadków pozytywnych i negatywnych.

Cel jest taki, że konfiguracja powiązania klas, która jest używana w kodzie testowym i produkcyjnym (taka sama) powoduje, że funkcjonalność jako całość działa.

Co mockować?

Oczywiste jest to, że testując szkołą Londyńską nasze testy mogą być bardzo ciężkie do wykonania. Jeżeli istnieje fragment w kodzie, który decyduje o czymś na podstawie wielu czynników, a następnie wykonuje operację z pewnym parametrem – przygotowanie permutacji danych i sprawdzenie możliwych wyników może być trudne.

Oddzielenie decyzji od konsekwencji

W takiej sytuacji można zrefaktoryzować kod osobno na:

  1. logikę podjęcia decyzji oraz
  2. na wykonanie akcji (konsekwencji_ na podstawie podjętej decyzji.

Klasy czysto procesowe

Dobrym kandydatem do mockowania są klasy typowo procesowe, czyli wywołujące poszczególne inne serwisy w zależności od podanego obiektu. Wtedy dobrym pomysłem jest mockowanie wszystkich zależności, przetestownaie ich gdzieś indziej, a klasę procesową zweryfikoać czy odpowiednio je wywołuje.

Czego nie mockować?

Nadużywanie mocków to niedobra praktyka.

Zdecydowanie nie polecam mockowania obiektów typu dane, value object. Do ich stworzenia służą Test Data Builders – czytaj więcej. Nie ma sensu tworzyć mocka obiektu, aby wysterować kilka getterów. Jeżeli logika, która opiera się na obiekcie wyciągnie z niego jakiś inne dane – musimy przeprojektować test. W przypadku, kiedy nie będziemy mockować danych, ewentualnie musimy zmienić dane testowe – co jest naturalną rzeczą.

Inną rzeczą, której nie powinniśmy mockować to prywatne metody, albo logiki agregatów. Jeżeli testujemy jakąś klasę i nie jesteśmy w stanie doprowadzić jej do stanu, aby ją przetestować i w tym celu musimy zamockować działanie prywatnej metody – coś tu nie gra. Najprawdopodobniej nie jest to dobry design kodu, są pomieszane odpowiedzialności, albo ciężki do zmiany stan wewnętrzny. Wtedy warto zastanowić się nad dekompozycją.

Nie próbujemy też mockować metod statycznych. Metody statyczne powinny działać zawsze w ten sam sposób, nie przechowywać stanu i nie wywoływać efektów ubocznych. Są tak proste, że mogą być użyte ponownie w innych fragmentach kodu w ramach zasady DRY (Don’t Repeat Yourself), ale wtedy stają się częścią tamtej logiki.

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.

.

Wpis który czytasz to zaledwie fragment wiedzy zawartej w Programie szkoleniowym Java Developera od SoftwareSkill. Mamy do przekazania sporo usystematyzowanej wiedzy z zakresu kluczowych kompetencji i umiejętności Java Developera. Program składa się z kilku modułów w cotygodniowych dawkach wiedzy w formie video.

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 👌

Czym jest Unit w Unit Testach?

W projekcie należy ustalić, czym jest „unit” (jednostka). Istnieją dwie szkoły pisania testów: 1. Moglibyśmy popatrzeć na testy że unitem jest klasa + jej prywatne implementacje. 2. Z drugiej strony moglibyśmy uznać, że unitem jest pakiet i publiczne API w pakiecie. W testach nie są widoczne bloki budulcowe, z jakich zbudowany jest pakiet, tylko publiczne API.

Dwie szkoły pisania testów: Detroit (amerykańska) i London (angielska)

Według szkoły Detroit najpierw zaczynamy pisać testy, a design i podział wewnętrzny jest naturalną pochodną. Nie mockujemy wewnętrznych zależności i testujemy to, co widać „z zewnątrz”. Wewnętrzne podziały i interakcje nie mają znaczenia.

Według szkoła Londyńskiej, unitem jest klasa. Każda interakcja z sąsiednią klasą jest zamockowana, lub wywołanie jej jest weryfikowane. Idea jaka stoi za tym podejściem jest taka, że jeżeli każda klasa jest osobno przetestowana, można ją zamockować, bo wynik jej działania został przetestowany gdzieś indziej, a w naszym teście wykorzystujemy ten ustalony wynik.

Mockować, czy nie?

Przeczytaj wpis, aby znaleźć wskazówki, co warto mockować, a czego nie.

Dyskusja