Sytuacja kobiet w IT w 2024 roku
22.07.20207 min

Testy jednostkowe - najlepsze praktyki

Sprawdź, jaką postawę i nawyki należy przyjąć, aby Twoje testy jednostkowe przyniosły Ci więcej korzyści, niż szkód.

Testy jednostkowe - najlepsze praktyki

Disclaimer: piszę tutaj o rzeczach, które są według mnie bardzo przydatne podczas pisania testów jednostkowych. Określam je mianem dobrych praktyk, bo pozwalają mi pisać dobre, wysokiej jakości testy, które są łatwiejsze do odczytania, utrzymania i lepiej opisują potrzeby biznesowe. Poniższe punkty są subiektywne - zdaję sobie sprawę, że możesz mieć inne zdanie na ten temat.

Testy podczas developmentu

Testy są bardzo ważne w procesie developmentu i dają wiele korzyści:

  • Potwierdzają wymagania.Pokazuje to, że Twoja implementacja poprawnie rozwiązała problem.
  • Identyfikują wady na wczesnych etapach.Zawsze lepiej jest znaleźć problemy wcześniej, ponieważ będą łatwiejsze i mniej kosztowne do naprawienia. Znalezienie defektu podczas programowania poprzez pisanie testów jest dobre.
  • Sprawiają, że utrzymanie jest łatwiejsze.Aby pisać testy, kod źródłowy powinien być testowalny, co oznacza, że będzie łatwiejszy w utrzymaniu. Testowalny kod jest złożony z oddzielonych między sobą komponentów, co zwiększa czytelność. Sprawia to również, że architektura jest lepsza.
  • Sprawiają, że refaktoryzacja jest bezpieczniejsza.Pozwalają one na duże zmiany, z potwierdzeniem, że nie wprowadzono żadnych regresji.
  • Pomagają w code review.Pokazują one jasno intencje autora i łatwiej jest wcześnie zweryfikować, czy działają one tak, jak powinny. Zapewni to lepszy wgląd w to, co faktycznie zostało zrobione, ułatwiając review.

Cechy dobrych testów

Zacznijmy od zdefiniowania „dobrego testu”.

Test można zwykle uznać za dobry, gdy jest:

  • Godny zaufania.Oznacza to, że zawodzi tylko, gdy to, co testuje, jest zepsute. Jeśli testy czasami kończą się niepowodzeniem, to znaczy, że są niestabilne i nie można ich określić mianem dobrych.
  • Czytelny / możliwy do utrzymania.Test powinien jasno określić, co testuje i jak jest wykonywany. Nie powinien mieć nadmiarowego kodu, skomplikowanych poprawek stanu czy kontroli przepływu.
  • Powinien weryfikować pojedynczy przypadek użycia.Jest to związane z zasadą jednej odpowiedzialności (ang. single responsibility principle). Jeśli test weryfikuje wiele przypadków, to jeśli się nie powiedzie, nie możemy dokładnie powiedzieć dlaczego. Dobry test weryfikuje pojedynczy przypadek użycia, a gdy się nie powiedzie, od razu będziemy wiedzieć, co poszło nie tak.
  • Odizolowany.Test nie powinien mieć wpływu na inne testy. Oznacza to w szczególności, że testy nie powinny mieć wspólnego stanu globalnego. Jeśli testy nie są izolowane, kolejność ich wykonywania może doprowadzić do nieoczekiwanych rezultatów.

Co oznacza napisać dobry test

Ta sekcja dotyczy najlepszych praktyk testowania. Proces testowy jest dobry, jeśli:


Testy są zautomatyzowane (na CI)

Testy są przydatne tylko wtedy, gdy są wykonywane w odpowiednim momencie, czy wtedy, gdy jest to potrzebne. Najlepszą opcją jest użycie ciągłej integracji (ang. continuous integration), która będzie stale uruchamiać testy (na przykład przy każdym commicie). W przeciwnym razie łatwo jest zapomnieć o uruchomieniu testów, co czyni je bezużytecznymi.


Testy są pisane podczas developmentu, a nie po

TDD, czyli test-driven development (kiedy piszesz testy przed napisaniem kodu) jest świetne, ale na samym początku może nie być łatwo przewidzieć, jak powinien Twój moduł wyglądać oraz jaka będzie struktura klas i tak dalej. Tak więc, jeśli nie da się napisać testów na początku - w porządku. Ważne jest, aby tworzyć testy na jak najwcześniejszym etapie developmentu i nie przekładać ich na koniec.

Testy pomagają pisać czysty kod. Podziel odpowiedzialność, używaj interfejsów, aby nie wystawiać szczegółów implementacji lub rzeczy zależnych od platformy. Jeśli opóźnisz pisanie testów, możesz znaleźć się w sytuacji, w której jakiegoś kodu nie będzie się dało przetestować i bardzo kuszące byłoby napisanie jakiegoś obejścia.


Testy są dodawane dla każdego zidentyfikowanego problemu

Pisząc testy, nie musisz zajmować się wszystkimi sytuacjami, które teoretycznie mogą się zdarzyć (omówimy to szczegółowo poniżej). Najważniejszą rzeczą jest odzwierciedlenie biznesowych przypadków użycia i ciągłe dodawanie testów pod kątem wszelkich innych znalezionych wymagań lub defektów.

Jest to szczególnie przydatne w przypadku defektów w kodzie. W ten sposób możesz przed naprawieniem zweryfikować, czy test naprawdę się nie powiódł i sprawdzić, czy po naprawieniu kodu faktycznie przeszedł.

Dobre praktyki testowania

Powyższe można potraktować, jako wstęp teoretyczny. Dobrze jest też wyrobić sobie pewne nawyki, jeżeli chodzi o testowanie. W tym artykule chciałbym jednak bardziej szczegółowo omówić kilka praktycznych kwestii, które mogą poprawić wyniki testów.


Dobrze nazwij swój test

Nazwa powinna opisywać, co jest testowane, w jakich warunkach test się odbywa i czego oczekuje się od wyniku. Jeśli istnieje spisany przypadek testowy, podaj link do niego w testowym Javadoc.

Dopiero, jeżeli nazwa testu będzie zbyt długa, możesz posłużyć się skrótami, ale pamięta,j by opisać ich znaczenie w Javadoc testu. Zła nazwa testu sprawia, że jest on trudniejszy do utrzymania. 


Przetestuj publiczny interfejs

Nie należy testować czegokolwiek, co nie jest publiczne. Nie psuj enkapsulacji (dostarczając @VisibleForTesting lub coś podobnego), aby przetestować daną funkcję.

Jeśli istnieje metoda, którą chcesz dokładnie i osobno przetestować, oznacza to, że najprawdopodobniej powinna być ona częścią publicznego interfejsu innej klasy (lub metody pomocniczej albo rozszerzenia). Każda testowana klasa powinna mieć publiczny interfejs (aby jasno określić, co powinno być testowane).

Testowanie niepublicznych rzeczy sprawia, że test jest trudniejszy do utrzymania, a zerwanie hermetyzacji rujnuje architekturę.


Sprawdzaj jeden przypadek użycia na test

Test powinien sprawdzić jedną rzecz. Oznacza to, że każdy test powinien mieć tylko jedną asercję. Są tutaj jednak wyjątki. Jeśli chcesz sprawdzić, czy konfiguracja testu została faktycznie zrobiona poprawnie, to możesz użyć domyślnych checków.

Jeśli chcesz sprawdzić, które metody w mockach zostały podczas testu wywołane (lub nie), to posiadanie kilku weryfikacji jest w tym przypadku OK. Chociaż w przypadku asercji ważne jest, aby używać jej raz na test.

Testowanie wielu rzeczy w jednym teście nie pozwala ci jasno powiedzieć, dlaczego się nie powiódł.


Pogrupuj treść testu w sekcje logiczne

W przypadku prostego testu jednostkowego, który potwierdza zwracanie wartości, powinna istnieć oddzielna sekcja z ustawieniem i sekcja z asercją. W przypadku skomplikowanego testu (takiego jak, np. test integracyjny), powinniśmy mieć sekcję konfiguracyjną (given), sekcję wyzwalającą (when) oraz sekcję rezultatów (then). 

Testy bez logicznego grupowania są mniej czytelne i, co za tym idzie, trudniejsze do utrzymania.  


Używaj odwrócenia zależności

Dostarczaj zależności do testowanych klas w konstruktorze lub przez publiczny interfejs. Nie twórz zewnętrznych zależności wewnątrz klasy. Nie pobieraj instancji singletonów wewnątrz klasy. Opakuj klasy związane z systemem/platformą swoim własnym interfejsem, zamiast podawać prawdziwe klasy danej platformy jako zależność. Oznacza to również dostarczenie interfejsu do pracy z kalendarzem czy czasem.

Nieużywanie DI sprawia, że Twój kod jest mniej testowalny. 


Mocki vs. stuby

Używaj prawdziwych klas, gdy jest to możliwe. Jeśli nie jest to możliwe, to dostarcz stub. Jeśli nie jest to jednak możliwe, to pracuj z mockiem. Oznacza to najczęściej dostarczenie prawdziwych encji, albo obiektów z danymi, wewnętrzne serwisy powinny być albo prawdziwe albo dostarczone jako stuby. Serwisy zewnętrzne powinny być stubami lub mockami.

Nadmierne i nieumiejętne używanie mocków w testach może sprawdzić, że testowana będzie implementacja mocka, a nie prawdziwa implementacja.


Domyślne buildery obiektów encji lub value objects

Używając prawdziwych klas dla encji lub value objects, warto mieć buildery z domyślnymi wartościami, aby je konstruować. Tak właśnie wyglądałaby domyślna implementacja encji / value objectu, w której można zmieniać istotne dla danego testu właściwości. 

Brak takich builderów prowadzi do duplikowania kodu i sprawia, że testy są trudniejsze w utrzymaniu. 


Grupy testów połączone w podklasy

Stwórz abstrakcyjną klasę testu bazowego z powszechną konfiguracją i stwórz podklasy, które testują konkretną część danej funkcji. Pogrupuje to mocno powiązane przypadki testowe w jednym miejscu. W taki sposób możliwe jest, aby wydobyć część nazw testów (część, która się powtarza) w otaczającą nazwę klasy. 

Posiadanie wszystkich testów wewnątrz pojedynczej klasy redukuje czytelność.


Początkowy stan testów powinien być generowany jedynie przez publiczne API testowanej klasy i jej zależności

Nie powinno się wprowadzać żadnych wewnętrznych zmian w klasach, aby stworzyć konfigurację testową. Żadnego @VisibleForTesting, które psuje hermetyzację. 

Zmiana wewnętrznego stanu przez niepubliczne API może stworzyć coś, z czym możemy sobie nie poradzić. Co więcej, jak już wspomniałem, psuje to hermatyzację. 


Twórz testy wcześnie

Napisz kilka testów, które sprawdzają podstawowe funkcje. Dodaj z czasem więcej testów, kiedy architektura się ustabilizuje i uzyskasz więcej informacji.  

Wczesne pisanie większości testów wymaga już wyższego poziomu umiejętności (jeżeli chodzi o TDD). Test-driven development jest świetne, ale ktoś niedoświadczony może potem przepisywać testy w miarę, jak zmienia się struktura. 

Przesuwanie testów na później, zwłaszcza na koniec procesu developmentu, może prowadzić do tego, że kod będzie nie do przetestowania. 

Podsumowanie

Pisanie testów nie jest łatwym zadaniem i wymaga dyscypliny. Co więcej, testy to też kod, który powinno się pisać z taką samą ostrożnością jak kod produkcyjny. Jednak, kiedy inwestujesz czas w testy, to z czasem będziesz miał z nich więcej pożytku. Nie bój się pisania testów i nie zwlekaj z tym. Zacznij już dzisiaj i cały czas z nimi pracuj. 

Przyjemnego kodowania!
Vasya Drobushkov


Oryginał tekstu w języku angielskim możesz przeczytać tutaj

<p>Loading...</p>