Java Developer? Przejd藕 na wy偶szy poziom wiedzy 馃敟馃挭 聽Sprawd藕

Team Leader? Podnie艣 efektywno艣膰 swojego zespo艂u 馃憣聽Sprawd藕

Mockowa膰 czy nie? 馃 Czym jest Unit w unit testach? Dwie szko艂y pisania test贸w

utworzone przez Java, Testowanie

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