Testy jednostkowe od podstaw - zasady, mocki i dobre praktyki

Bieg na 1 i 2 torze, symbolizujący dobre praktyki dla testów jednostkowych.

Napisano przez

Eryk Pawlak

Opublikowano

6 paź 2026

Spis treści

Gdy po niewielkiej zmianie w kodzie nagle przestaje działać rabat, logowanie albo walidacja formularza, szybki test pojedynczej funkcji potrafi oszczędzić godziny poszukiwań. W tym artykule wyjaśniam, czym są unit tests, jak projektować testy jednostkowe, kiedy stosować mocki oraz gdzie kończą się możliwości tej metody.

Najważniejsze zasady dają się zastosować od pierwszego testu

  • Test jednostkowy sprawdza mały fragment logiki w izolacji od bazy danych, sieci i systemu plików.
  • Dobry test jest szybki, powtarzalny i czytelny, a jego wynik nie zależy od kolejności uruchomienia.
  • Najpraktyczniejszy schemat pracy to Arrange, Act, Assert, czyli przygotowanie, wykonanie i sprawdzenie rezultatu.
  • Mocki, stuby i fake’i zastępują zależności, ale ich nadmiar może sprawić, że test zacznie sprawdzać implementację zamiast zachowania.
  • Testy jednostkowe nie zastępują testów integracyjnych i end-to-end. Najlepsze efekty daje połączenie kilku poziomów testowania.

Czym są testy jednostkowe i co naprawdę sprawdzają

Test jednostkowy służy do sprawdzenia małej, logicznie wyodrębnionej części programu. Może nią być funkcja obliczająca cenę po rabacie, metoda sprawdzająca poprawność adresu e-mail albo komponent odpowiadający za konkretną regułę biznesową.

Najważniejsze jest słowo „izolacja”. Podczas testu nie chcę łączyć się z prawdziwą bazą danych, wysyłać żądania do zewnętrznego API ani zapisywać plików na dysku. Zależności zastępuję kontrolowanymi obiektami testowymi, dzięki czemu wiem, że wynik pochodzi z badanej logiki, a nie z przypadkowego stanu środowiska.

Typowy test powinien odpowiadać na jedno konkretne pytanie. Przykładowo, zamiast sprawdzać całą obsługę zamówienia, testuję osobno, czy funkcja nalicza darmową dostawę od określonej kwoty. Taki test łatwiej przeczytać, uruchomić i naprawić po zmianie kodu.

Trzy elementy dobrego testu

W praktyce najwygodniej korzystać ze schematu Arrange, Act, Assert. Najpierw przygotowuję dane i zależności, potem wywołuję badaną funkcję, a na końcu sprawdzam oczekiwany rezultat.

  1. Arrange tworzy dane wejściowe i konfigurację.
  2. Act wykonuje jedną operację, którą chcę sprawdzić.
  3. Assert porównuje wynik z oczekiwaniem.

Jeśli w jednym teście pojawia się kilkanaście asercji dotyczących różnych zachowań, zwykle jest to sygnał, że test stał się zbyt szeroki. Wyjątkiem może być test kilku powiązanych właściwości jednego wyniku, ale nawet wtedy warto zachować umiar.

Dlaczego opłaca się je pisać, ale nie traktować jak tarczy

Największą korzyścią jest szybka informacja zwrotna. Kiedy zmieniam kod i test jednostkowy przestaje przechodzić, dostaję komunikat blisko miejsca, w którym pojawił się problem. Nie muszę czekać na pełne wdrożenie ani ręcznie przeklikiwać całej aplikacji.

Testy pełnią też funkcję żywej dokumentacji. Dobrze nazwany przypadek pokazuje, jak system powinien zachować się dla pustych danych, wartości granicznych lub błędnych parametrów. Z tego powodu często traktuję test jako przykład użycia funkcji, a nie wyłącznie jako zabezpieczenie przed regresją.

Jest jeszcze jedna korzyść, o której początkujący często przekonują się dopiero po czasie. Kod trudny do przetestowania bywa też trudny do rozwijania, bo ma zbyt wiele ukrytych zależności. Pisanie testów wymusza więc czytelniejszy podział odpowiedzialności i częściej prowadzi do prostszej architektury.

Nie oznacza to jednak, że same testy jednostkowe wykryją każdy błąd. Mogą potwierdzić, że moduł poprawnie reaguje na przygotowane dane, ale nie sprawdzą, czy aplikacja właściwie komunikuje się z bazą, czy endpoint ma poprawną konfigurację albo czy użytkownik może przejść cały proces zakupowy.

Poziom testu Co sprawdza Typowa zaleta Ograniczenie
Jednostkowy Pojedynczą funkcję, klasę lub regułę Krótki czas wykonania i precyzyjny błąd Nie potwierdza działania całego systemu
Integracyjny Współpracę kilku modułów lub usług Wykrywa problemy na granicach komponentów Jest wolniejszy i trudniejszy w konfiguracji
End-to-end Pełny scenariusz z perspektywy użytkownika Sprawdza działanie aplikacji jako całości Bywa kosztowny, wolny i podatny na niestabilność

Dlatego nie przywiązuję się do jednej magicznej wartości pokrycia kodu. 90 procent pokrycia nie gwarantuje jakości, jeśli testy sprawdzają wyłącznie proste linie i pomijają błędy graniczne. Lepiej mieć mniej testów, ale dobrze dobranych do ryzyka biznesowego, niż ścigać samą statystykę.

Piramida testów: na dole szybkie i tanie unit tests, wyżej integracyjne, na górze E2E - wolne i drogie.

Jak zaprojektować dobry test krok po kroku

Zaczynam od zachowania, a nie od implementacji. Najpierw zapisuję, co użytkownik lub inny moduł powinien otrzymać w konkretnej sytuacji. Dopiero później zastanawiam się, jak zbudować asercję.

1. Wybierz małą regułę

Załóżmy, że aplikacja oblicza cenę po rabacie. Warto rozdzielić przypadki, zamiast tworzyć jeden test obejmujący cały koszyk. Interesują mnie między innymi brak rabatu, poprawny rabat i wartość graniczna.

2. Opisz oczekiwane zachowanie

Nazwa testu powinna mówić, co się wydarzyło i jaki rezultat jest prawidłowy. Nazwa w rodzaju „test ceny” niewiele wyjaśnia. Znacznie lepiej brzmi „zwraca cenę obniżoną o 10 procent dla aktywnego kodu rabatowego”.

3. Sprawdź rezultat, nie każdy szczegół wykonania

Przykład w JavaScript może wyglądać tak:

it('zwraca cenę po zastosowaniu rabatu', () => {
  const cena = obliczCene(200, 10);

  expect(cena).toBe(180);
});

Ten test jest krótki, ale ma sens tylko wtedy, gdy jasno wiadomo, co oznacza drugi argument funkcji. Jeśli kod dopuszcza rabaty ujemne, większe niż 100 procent albo wartości tekstowe, trzeba dodać osobne przypadki opisujące te reguły.

4. Dodaj przypadki graniczne

Najwięcej błędów kryje się na granicach, nie w oczywistym środku zakresu. Dla funkcji finansowej sprawdzę zero, najmniejszą dozwoloną kwotę, maksymalny rabat i niepoprawne dane. W aplikacji użytkowej warto też uwzględnić pustą listę, brak uprawnień i powtórne wykonanie tej samej operacji.

  • prawidłowe dane wejściowe,
  • puste lub zerowe wartości,
  • wartości poniżej i powyżej limitu,
  • niepoprawny typ danych,
  • wyjątek albo komunikat błędu.

Nie każdy przypadek wymaga osobnego testu. Jeśli kilka danych prowadzi do dokładnie tej samej reguły, można użyć testów parametryzowanych. Dzięki temu kod testu pozostaje mały, a lista scenariuszy jest widoczna w jednym miejscu.

Izolacja, mocki i test doubles bez chaosu

W testach jednostkowych często pojawiają się test doubles, czyli zastępcze obiekty udające prawdziwe zależności. Dzięki nim funkcja może „rozmawiać” z bazą danych lub usługą płatniczą bez uruchamiania tych systemów.

Przeczytaj również: Czarna skrzynka - Prawdziwe nazwy i testy rejestratorów lotu

Stub, mock i fake

Stub dostarcza przygotowaną odpowiedź. Użyję go wtedy, gdy chcę sprawdzić, jak kod reaguje na konkretny wynik, na przykład informację o dostępności produktu.

Mock pozwala dodatkowo zweryfikować, czy zależność została wywołana w oczekiwany sposób. Może sprawdzić liczbę wywołań, przekazane argumenty albo kolejność operacji. Tu łatwo przesadzić, bo test zaczyna kontrolować szczegóły implementacji zamiast zachowania.

Fake jest uproszczoną, działającą wersją zależności. Przykładem może być pamięciowa baza danych używana tylko podczas testu. Zwykle daje bardziej realistyczne zachowanie niż sam mock, ale wymaga większego przygotowania.

Moja praktyczna zasada jest prosta. Zastępuję zależność wtedy, gdy jest wolna, zewnętrzna, losowa albo trudna do kontrolowania. Nie tworzę mocka dla każdej prywatnej metody, bo taki test staje się kruchy i wymaga zmian przy niemal każdej refaktoryzacji.

Najłatwiej osiągnąć izolację przez wstrzykiwanie zależności. Zamiast tworzyć klienta API bezpośrednio wewnątrz klasy, przekazuję go z zewnątrz. W produkcji otrzymuję prawdziwego klienta, a w teście kontrolowany zamiennik.

Narzędzia i uruchamianie testów w zespole

Dobór frameworka zależy głównie od języka i ekosystemu. Samo narzędzie nie naprawi źle zaprojektowanych testów, ale powinno zapewniać czytelne asercje, izolację, raportowanie oraz wygodne uruchamianie pojedynczych przypadków.

Technologia Popularne rozwiązania Kiedy sprawdzają się najlepiej
JavaScript i TypeScript Jest, Vitest Aplikacje webowe, biblioteki i projekty frontendowe
Python pytest, unittest Backend, automatyzacja, dane i skrypty
Java JUnit Systemy backendowe i aplikacje korporacyjne
C# xUnit, NUnit, MSTest Projekty .NET i usługi działające na platformie Microsoft
C++ GoogleTest Oprogramowanie systemowe, embedded i biblioteki natywne

Testy powinny uruchamiać się automatycznie przy każdym pull requeście i przed wdrożeniem. W ciągłej integracji liczy się nie tylko wynik „zielony” lub „czerwony”, ale też informacja, który scenariusz się zepsuł i czy problem jest powtarzalny.

Warto rozdzielić szybki zestaw testów jednostkowych od wolniejszych testów integracyjnych. Programista może uruchamiać te pierwsze po każdej zmianie, a pełny zestaw wykonywać w pipeline. Takie rozdzielenie skraca pętlę informacji zwrotnej bez rezygnacji z szerszej kontroli jakości.

Test-driven development, czyli TDD, polega na napisaniu testu przed implementacją. Nie traktuję tej techniki jako obowiązku dla każdego zadania. Jest szczególnie użyteczna przy regułach biznesowych, parserach i algorytmach, natomiast przy szybkim prototypie interfejsu może spowalniać pracę, zanim wymagania się ustabilizują.

Błędy, które odbierają testom wartość

Najczęstszy problem to test zależny od środowiska. Jeśli raz przechodzi, a raz nie, bo korzysta z aktualnej daty, losowej wartości, wspólnego pliku lub zewnętrznego API, zespół szybko zaczyna go ignorować. Taki test nazywa się często flaky testem, czyli testem niestabilnym.

Drugim błędem jest sprawdzanie prywatnej implementacji zamiast publicznego zachowania. Gdy zmiana nazwy zmiennej albo sposobu wywołania metody wymaga poprawiania wielu testów, prawdopodobnie testy są zbyt mocno związane z kodem.

  • Wspólny stan między testami powoduje, że kolejność uruchamiania zaczyna mieć znaczenie.
  • Zbyt wiele mocków daje pozorne poczucie izolacji, ale nie potwierdza realnej współpracy modułów.
  • Jedna asercja na wszystko utrudnia znalezienie przyczyny błędu.
  • Testowanie kodu testowego rozbudowuje zestaw bez zwiększania pewności.
  • Ślepe dążenie do 100 procent pokrycia może prowadzić do testów pozbawionych wartości.

Nie warto też kopiować całej logiki produkcyjnej do testu. Jeśli test sam oblicza oczekiwany wynik tym samym algorytmem co aplikacja, oba fragmenty mogą zawierać identyczny błąd. Oczekiwanie powinno być proste i niezależne, nawet jeśli oznacza ręczne wpisanie kilku wartości.

Gdy test przestaje przechodzić po zmianie wymagania, nie zawsze oznacza to błąd w kodzie. Czasem test prawidłowo ujawnia, że zmieniła się reguła biznesowa. Wtedy trzeba zaktualizować zarówno implementację, jak i scenariusz, zachowując jego pierwotny sens.

Najmniejszy sensowny plan wdrożenia testów jednostkowych

Nie zaczynałbym od próby pokrycia całego istniejącego systemu. Lepszy efekt daje wybranie jednego modułu o wysokim ryzyku, na przykład naliczania opłat, autoryzacji albo przetwarzania zamówień, i opisanie jego najważniejszych reguł.

Na początku wystarczy kilka testów dla poprawnego scenariusza, wartości granicznych i błędnych danych. Potem warto uruchamiać je w CI, usuwać niestabilność i dodawać kolejne przypadki zawsze wtedy, gdy błąd przedostał się do środowiska testowego lub produkcyjnego.

Dobre testy jednostkowe nie są ozdobą projektu ani celem samym w sobie. Są krótkim, automatycznym kontraktem opisującym zachowanie kodu. Jeśli są szybkie, niezależne i skupione na realnych regułach, pomagają bezpieczniej refaktoryzować aplikację i szybciej reagować na zmiany.

FAQ - Najczęstsze pytania

Najpierw przygotuj dane i zależności w części Arrange, następnie wykonaj jedną badaną operację w Act, a na końcu porównaj rezultat z oczekiwaniem w Assert. Test powinien odpowiadać na jedno konkretne pytanie i sprawdzać przede wszystkim zachowanie funkcji.

Stub dostarcza przygotowaną odpowiedź, mock dodatkowo sprawdza sposób wywołania zależności, a fake jest uproszczoną, działającą wersją zależności, na przykład pamięciową bazą danych. Zastępuj zależności, gdy są wolne, zewnętrzne, losowe albo trudne do kontrolowania, ale unikaj mockowania każdej prywatnej metody.

Uwzględnij między innymi zero, najmniejszą dozwoloną kwotę, maksymalny rabat, wartości poniżej i powyżej limitu, niepoprawne typy danych oraz wyjątki lub komunikaty błędów. W aplikacjach użytkowych mogą być istotne także puste listy, brak uprawnień i powtórne wykonanie operacji.

Test jednostkowy sprawdza pojedynczą funkcję, klasę lub regułę w izolacji i szybko wskazuje błąd. Test integracyjny bada współpracę kilku modułów lub usług, a test end-to-end sprawdza pełny scenariusz z perspektywy użytkownika. Najlepszą kontrolę jakości daje połączenie tych poziomów, ponieważ testy jednostkowe nie potwierdzają działania całego systemu.

Oceń artykuł

Ocena: 4.33 Liczba głosów: 3

Tagi:

testy jednostkowe tdd testy end-to-end mocki integracja ciągła

Udostępnij artykuł

Eryk Pawlak

Eryk Pawlak

Nazywam się Eryk Pawlak i od 7 lat zajmuję się automatyzacją testów oraz zapewnieniem jakości oprogramowania. Moja przygoda z tym obszarem zaczęła się z fascynacji technologią i chęcią tworzenia produktów, które są nie tylko funkcjonalne, ale także niezawodne. Interesuje mnie, jak dzięki odpowiednim narzędziom i metodom można uprościć skomplikowane procesy testowe, a także jak ważne jest ciągłe doskonalenie jakości w projektach IT. W swojej pracy skupiam się na dostarczaniu rzetelnych, zrozumiałych i aktualnych informacji, które mogą pomóc innym w zrozumieniu wyzwań związanych z QA. Lubię porównywać różne podejścia do automatyzacji, a także analizować trendy w branży, aby móc dzielić się sprawdzonymi rozwiązaniami. Moim celem jest, aby każdy mógł odnaleźć w moich tekstach wartościowe wskazówki, które ułatwią im pracę w obszarze jakości oprogramowania.

Napisz komentarz