Pytanie o to, jak pisać testy jednostkowe aplikacji, pada w każdym zespole, który przekroczył kilka tysięcy linii kodu i zaczął tracić kontrolę nad regresjami. Odpowiedź na to, jak pisać testy jednostkowe aplikacji, nie sprowadza się do wyboru frameworka ani do pogoni za wskaźnikiem pokrycia. Test jednostkowy to najmniejszy możliwy kontrakt: bierze jedną funkcję, podaje jej konkretne dane wejściowe i sprawdza dokładnie jeden efekt. Jeśli w projekcie masz moduł zamieniający formularz kontaktowy na wiadomość e-mail, jednostką jest funkcja walidująca adres, a nie cała ścieżka od kliknięcia do skrzynki odbiorczej. Dobrze napisany zestaw uruchamia się w kilka sekund, działa bez internetu, bez bazy danych i bez serwera SMTP, a jego wynik jest identyczny na laptopie juniora i na maszynie CI. Ten przewodnik pokazuje warstwa po warstwie, jak zbudować taki zestaw od zera i jak utrzymać go przez lata.
Czym jest jednostka i co naprawdę testujesz
Jednostka to najmniejszy fragment kodu, który da się wywołać niezależnie: funkcja, metoda albo mała klasa bez stanu globalnego. Test jednostkowy nie dotyka bazy danych, dysku ani sieci. Jeśli musisz podnieść kontener z PostgreSQL, żeby cokolwiek sprawdzić, piszesz test integracyjny i powinieneś nazwać go zgodnie z prawdą. Ta granica jest podstawowym rozstrzygnięciem całej dyscypliny.
Weźmy realny przykład z panelu administracyjnego dla webmasterów. Moduł parsujący odpowiedź WHOIS ma jedną odpowiedzialność: zamienić surowy tekst na obiekt z datą wygaśnięcia, nazwą rejestratora i statusem. Testujesz ten parser na dziesięciu zapisanych próbkach odpowiedzi, a nie odpytujesz prawdziwego rejestru przy każdym uruchomieniu zestawu.
Nie testuj gettera, który zwraca pole, ani frameworka, za który płacisz zespołowi zewnętrznemu. Testuj logikę własną: warunki brzegowe, obsługę pustych kolekcji, zaokrąglenia kwot, konwersje stref czasowych, format daty. Reguła praktyczna brzmi: jeśli test nie potrafiłby wykryć żadnej realnej pomyłki programisty, jest kosztem, a nie zabezpieczeniem.
Trzecia kategoria to kod, który wygląda banalnie, a psuje się najczęściej. Funkcje obliczające ceny netto i brutto przy stawce 23 procent, generatory slugów usuwające polskie znaki, walidatory numerów telefonów w formacie międzynarodowym. Każda z nich mieści się w dziesięciu liniach i każda z nich potrafi położyć produkcję po niewinnym refaktorze.
Środowisko uruchomieniowe i infrastruktura pod testy
Testy jednostkowe uruchamiasz w trzech miejscach: lokalnie przed commitem, w hooku pre-push oraz w potoku CI po wypchnięciu gałęzi. Każde z tych miejsc musi używać tej samej wersji interpretera i tego samego pliku blokady zależności. Rozjazd wersji to najczęstsza przyczyna zdania mojego lokalnie działa, którego nikt w zespole nie chce słyszeć.
Infrastruktura pod runnera to osobna decyzja kosztowa. Pytanie o to, czym różni się VPS od hostingu współdzielonego, ma tu bardzo konkretną odpowiedź: konto współdzielone za 120–250 zł rocznie nie daje dostępu do powłoki ani do demona Dockera, więc nie postawisz na nim własnego runnera. VPS z 2 vCPU i 4 GB RAM kosztuje 40–90 zł miesięcznie i wystarcza dla zespołu do dziesięciu osób.
Kontekst hostingowy wchodzi też do samego kodu testów, gdy budujesz narzędzia dla klientów. Aplikacja doradcza podpowiadająca, jak wybrać hosting pod WordPress, opiera się na regułach punktujących limit PHP workers, wersję PHP i obecność cache obiektowego. Te reguły to czysta logika i nadają się do testów jednostkowych wręcz idealnie.
Runner lokalny, VPS czy usługa chmurowa
| Środowisko | Orientacyjny koszt | Czas zestawu 800 testów | Zastosowanie |
|---|---|---|---|
| Laptop dewelopera | 0 zł | 8–15 s | Pętla TDD, praca dzienna |
| Hosting współdzielony | 150 zł/rok | brak powłoki | Nieprzydatny dla CI |
| VPS 2 vCPU / 4 GB | 50 zł/mies. | 25–40 s | Własny runner zespołu |
| CI w chmurze | 0–80 zł/mies. | 40–70 s z instalacją | Weryfikacja pull requestów |
Zestaw testów jest również siatką bezpieczeństwa przy migracji. Gdy planujesz, jak przenieść stronę na inny hosting, uruchomienie pełnego zestawu na nowym środowisku wychwytuje różnice w wersji PHP, brakujące rozszerzenia i inne ustawienia strefy czasowej, zanim domena wskaże nowe adresy IP.
Anatomia dobrego testu: wzorzec AAA i nazewnictwo
Każdy test dzielisz na trzy bloki: przygotowanie danych, wykonanie jednej operacji i weryfikacja jednego oczekiwania. Ten wzorzec bywa nazywany Arrange–Act–Assert. Jeśli w metodzie testowej pojawiają się dwa wywołania testowanej funkcji i pięć asercji, to nie jest jeden test, tylko cztery sklejone w jeden nieczytelny blok.
Nazwa testu jest dokumentacją, którą czyta się w raporcie z czerwonym statusem. Zamiast test_parser napisz parser_zwraca_date_wygasniecia_dla_odpowiedzi_z_polskiego_rejestru. Po pół roku nikt nie będzie odtwarzał intencji z ciała metody, a osoba dyżurna zobaczy w logu CI od razu, która reguła biznesowa przestała obowiązywać.
Dane wejściowe trzymaj blisko testu i wypisuj je wprost. Ukrywanie wartości w rozbudowanych fabrykach oszczędza kilka linii, ale zmusza czytelnika do skakania po plikach. Testy parametryzowane rozwiązują to lepiej: jedna funkcja, tabela przypadków i czytelny opis każdego wiersza w wyniku uruchomienia zestawu.

- Parser rejestru odpowiadający na to, jak sprawdzić kto jest właścicielem domeny, zwraca nazwę abonenta lub wartość pustą dla danych ukrytych.
- Kalkulator okresu karencji dla pytania, jak przedłużyć domenę po wygaśnięciu, wylicza 30 dni odnowienia i 30 dni wykupu.
- Walidator strefy dla zagadnienia, jak skonfigurować rekordy DNS domeny, odrzuca wartość TTL poniżej 60 sekund.
- Reguła wyjaśniająca, co to jest rekord CNAME, blokuje jego ustawienie na domenie głównej obok rekordu MX.
Mocki, stuby i izolacja zależności zewnętrznych
Zależność zewnętrzna to wszystko, czego nie kontrolujesz w milisekundach: zegar systemowy, generator liczb losowych, system plików, HTTP. Zastępujesz je atrapami wstrzykiwanymi przez konstruktor lub parametr. Kod, który sam tworzy sobie klienta HTTP wewnątrz metody, jest niemal niemożliwy do przetestowania bez brzydkich sztuczek na poziomie modułu.
Moduł odnawiający certyfikaty pokazuje to najlepiej. Logika decydująca, czy darmowy certyfikat SSL Let’s Encrypt wymaga odnowienia, sprowadza się do porównania daty wygaśnięcia z progiem 30 dni. Podajesz jej sztuczny zegar i pięć przykładowych certyfikatów, a nie odpytujesz prawdziwego serwera ACME. Instrukcja opisująca, jak zainstalować certyfikat SSL na serwerze, jest wtedy testowana wyłącznie w warstwie generowania poleceń.
Ta sama zasada dotyczy generatorów konfiguracji. Funkcja odpowiadająca na pytanie, jak ustawić przekierowanie 301 w htaccess, przyjmuje listę par adresów i zwraca tekst reguł. Sprawdzasz kolejność wpisów, ucieczkę znaków specjalnych i brak pętli przekierowań, porównując wynik ze wzorcem, bez uruchamiania serwera Apache.
Kiedy atrapa zaczyna szkodzić
Nadmiar mocków zamienia test w kopię implementacji. Gdy sprawdzasz wyłącznie, że metoda zapisz została wywołana raz z określonym argumentem, każdy refaktor rozbija zestaw, mimo że zachowanie systemu pozostało identyczne. Testuj rezultaty i stan końcowy, a weryfikację interakcji zostaw dla efektów ubocznych, których inaczej nie zaobserwujesz.
Pokrycie kodu, potok CI i utrzymanie zestawu
Pokrycie mierzy, które linie wykonały się podczas przebiegu, a nie czy zachowanie jest poprawne. Realny cel dla większości projektów to 70–85 procent dla warstwy domenowej i znacznie mniej dla kodu sklejającego. Wymuszanie 100 procent generuje testy pisane pod metrykę, które sprawdzają istnienie kodu zamiast jego sensu.
Testy niestabilne niszczą zaufanie szybciej niż brak testów. Diagnozujesz je tak samo jak łącze: kilka pomiarów w różnych warunkach, zanim wyciągniesz wniosek. Osoba pytająca, jak sprawdzić prędkość internetu rzetelnie, powtarza pomiar o różnych porach i przewodowo, a nie po WiFi. Ktoś szukający odpowiedzi na to, jak wzmocnić sygnał WiFi w domu, najpierw ustala źródło zakłóceń, dopiero potem kupuje sprzęt.
Utrzymanie polega na traktowaniu kodu testów jak kodu produkcyjnego. Usuwaj duplikaty, wydzielaj pomocnicze asercje, kasuj testy opisujące wymagania, które przestały obowiązywać. Zestaw przekraczający dwie minuty przestaje być uruchamiany lokalnie, a wtedy cała pętla informacji zwrotnej przenosi się do CI i wydłuża z sekund do kwadransów.
Ostatni element to dyscyplina przy błędach. Każdy zgłoszony defekt zaczyna się od testu, który go odtwarza i jest czerwony przed poprawką. Dzięki temu pytanie o to, jak pisać testy jednostkowe aplikacji, przestaje być teorią, a staje się rutyną wpisaną w codzienny proces zespołu.
Jak zacząć pisać testy w projekcie, który ich nie ma
Nie planuj kilkumiesięcznej akcji pokrywania całego repozytorium, bo takie inicjatywy kończą się porzuceniem po trzech tygodniach. Zacznij od reguły granicznej: każdy nowy kod i każda poprawka błędu wchodzą z testem, reszta zostaje bez zmian. Następnie wybierz trzy do pięciu modułów o najwyższym ryzyku biznesowym, zwykle rozliczenia, uprawnienia i integracje płatnicze, i obłóż je testami charakteryzującymi, które opisują obecne zachowanie, nawet jeśli wydaje się dziwne. Taki test nie ocenia poprawności, tylko zamraża stan przed refaktorem. Dopiero mając siatkę bezpieczeństwa, rozbijasz duże funkcje na mniejsze jednostki i dopisujesz właściwe asercje. Po dwóch miesiącach zwykle masz 300–500 testów w miejscach, które faktycznie generowały zgłoszenia od klientów.
Czy testy jednostkowe zastępują testy integracyjne i manualne
Nie zastępują i nie zostały do tego zaprojektowane. Testy jednostkowe odpowiadają na pytanie, czy pojedynczy fragment logiki liczy poprawnie, i robią to w kilka milisekund. Nie wychwycą błędnego zapytania SQL, źle skonfigurowanego indeksu, złej kolejności migracji ani sytuacji, w której dwa poprawne moduły rozmawiają ze sobą niepoprawnie. Do tego służą testy integracyjne, uruchamiane na prawdziwej bazie w kontenerze, oraz testy end-to-end przechodzące przez interfejs w przeglądarce. Rozsądna proporcja to około 70 procent testów jednostkowych, 20 procent integracyjnych i 10 procent scenariuszy end-to-end, bo te ostatnie są najwolniejsze i najbardziej podatne na losowe awarie. Testy eksploracyjne wykonywane przez człowieka wciąż wykrywają problemy użyteczności, których żadna asercja nie opisze.
Co powinno znaleźć się w dobrym teście, a czego unikać
Dobry test ma jedną jasną intencję, opisową nazwę, dane wejściowe widoczne wprost w ciele metody i jedno logiczne oczekiwanie na końcu. Jest deterministyczny, czyli daje ten sam wynik przy tysiącu uruchomień, i niezależny, czyli działa poprawnie także wtedy, gdy uruchomisz go jako jedyny albo w losowej kolejności. Unikaj współdzielonego stanu między testami, uśpień czekających na cokolwiek, odwołań do bieżącej daty systemowej i połączeń sieciowych. Odrzuć też logikę warunkową wewnątrz testu, bo instrukcja if oznacza, że opisujesz dwa przypadki naraz i żaden z nich nie jest czytelny w raporcie. Gdy test wymaga komentarza wyjaśniającego, o co w nim chodzi, przepisz go zamiast dopisywać wyjaśnienie.

