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