Test jednostkowy (ang. unit test) to fragment kodu, który weryfikuje poprawność działania innego fragmentu kodu. Testowany fragment kodu jest poddawany testowi, który wykonuje go i w ramach weryfikacji porównuje wynik z oczekiwanym rezultatem. Zastanówmy się przez chwilę, skąd mamy mieć pewność, że testy jednostkowe zostały prawidłowo zaimplementowane w naszym projekcie. Zgodność testu jednostkowego z wymaganiami biznesowymi zależy od zrozumienia domeny i implementacji programisty. Oczywiście testy jednostkowe można zweryfikować testami mutacyjnymi, które sprawdzają jak dokładnie został pokryty kod źródłowy. Jednak to nie jest tematem tego artykułu. Na podstawie swojego doświadczenia i wiedzy przedstawię praktyki, które stosuję by testy jednostkowe były wiarygodne, łatwe w utrzymaniu i czytelne dla innych programistek/programistów w zespole.

F.I.R.S.T.

Zbiór pięciu zasad kryjących się pod akronimem S.O.L.I.D. jest każdemu programiście/programistce dobrze znany. Mniej osób natomiast kojarzy akronim F.I.R.S.T., który określa pięć podstawowych reguł dotyczących wytwarzaniu testów jednostkowych.

F (Fast) – testy powinny działać szybko. W przypadku, gdy testy jednostkowe będą wykonywały się długo sprawimy że będziemy je uruchamiać rzadziej lub wcale. Im mniejsza częstotliwość uruchamiania testów, tym później dowiadujemy się o błędach i wprowadzamy poprawkę.

I (Independent) – testy powinny być niezależne od siebie. W ciele testu nie powinna następować konfiguracja warunków, która wpływa na wykonanie następnego testu. Jeśli testy są odizolowane od siebie wtedy można je uruchomić w dowolnej kolejności, a wyniki z testów zawsze będą takie same.

R (Repeatable) – testy powinny być powtarzalne w każdym środowisku. Uruchamiając testy na swoim komputerze, na laptopie koleżanki, serwerze buildów powinniśmy za każdym razem otrzymać ten sam rezultat.

S (Self-Validating) – rezultat testu powinien być jednoznaczny (test powiódł się lub nie).

T (Timely) – testy powinny być pisane bezpośrednio przed tworzeniem testowanego kodu produkcyjnego.

Dobre praktyki

Osoby stawiające pierwsze kroki w pisaniu testów jednostkowych często tworzą zagmatwany kod testowy. Na pierwszy rzut oka wszystko wydaje się być dobrze, testy świecą się na zielono i pokrywają kod produkcyjny. Jednak zmiana wymagań klienta wiąże się z poprawą testu jednostkowego. Im gorzej napisany test, tym dłużej będziemy wprowadzać poprawkę. W pisaniu testów jednostkowych pomogą poniższe dobre praktyki:

  • W ramach projektu/zespołu powinna być stosowana jedna konwencja nazewnictwa testów jednostkowych. Nazwa testu powinna określać jednoznacznie, jaki przypadek testowy pokrywa dany test. Poniżej kilka przykładów konwencji nazewnictwa testów jednostkowych.

MethodName_StateUnderTest_ExpectedBehavior

MethodName_ExpectedBehavior_StateUnderTest

Should_ExpectedBehavior_When_StateUnderTest

When_StateUnderTest_Expect_ExpectedBehavior

Given_Preconditions_When_StateUnderTest_Then_ExpectedBehavior

  • Wzorzec Arrange-Act-Assert (AAA) i Given-When-Then (GWT). Kod testów powinien być ustandaryzowany według konwencji AAA lub GWT. W fazie Arrange/Given definiowane są dane wejściowe. W sekcji Act/When wywoływana jest akcja (metoda, komenda), którą chcemy przetestować. W bloku Assert/Then weryfikowana jest zgodność założeń z rzeczywistymi wynikami. Stosowanie powyższych konwencji powoduje, że testy są łatwiejsze w czytaniu.
  • Klasie z kodem produkcyjnym odpowiada jedna klasa z testami jednostkowymi.
  • Testy jednostkowe powinny być wykonywane w izolacji, wszystkie zewnętrzne zależności powinny być mockowane np. za pomocą NSubstitute. Wykonanie testów z zależnościami nazywamy testami integracyjnymi, które służą do wykrycia błędów w interfejsach i interakcjach między klasami.
  • Wykorzystanie wzorca builder do tworzenia bardziej złożonych obiektów testowych. Konstruowanie obiektów tylko z polami wymaganymi do testów poprawia czytelność testów. Unikamy wypisywania parametrów, które nie mają znaczenia dla wykonywanego testu jednostkowego.
  • Testy jednostkowe nie powinny zawierać instrukcji warunkowych. Dla każdej instrukcji warunkowej powinien zostać napisany osobny test jednostkowy.
  • Testy jednostkowe nie powinny zawierać pętli w teście do sprawdzania kolejnych przypadków testowych. W sytuacji, gdy dla jednej z asercji uzyskamy wynik negatywny, kolejne przypadki nie będą wykonane. Przykładowo NUnit umożliwia parametryzowanie metod testowych i przekazywanie danych do testów przez atrybut TestCase lub TestCaseSource. Parametryzacja metod testowych powoduje, że każdy przypadek testowy będzie traktowany, jako osobny test.
  • W klasie testowej w celu uniknięcia powielania kodu implementacji testowanej jednostki (ang. System Under Test) implementuje się prywatną metodę GetSUT zwracającą instancje testowanej klasy. W przypadku dorzucenia kolejnego parametru do konstruktora testowanej klasy, należy tylko zmienić metodę GetSUT, a nie jak w wcześniejszym przypadku każdy test jednostkowy.
  • W testach wyłapujemy jak najbardziej szczegółowe wyjątki.
  • Testujemy tylko publiczne metody.
  • W testach unikamy komentarzy i wyrzucania danych na konsole czy do pliku. Testy powinny być jednoznaczne, użytkownik nie powinien analizować dodatkowych danych w celu oceny poprawności testu.
  • Test powinien testować tylko jeden aspekt.

Podsumowanie

Testy jednostkowe są czasochłonne, ale stanowią dobrą dokumentacje projektu. Pisząc czytelne testy zgodnie z powyższymi regułami zwiększamy jakość projektu i przyczyniamy się także do tego, że kod produkcyjny jest bardziej przemyślany. U każdego z Was dobre praktyki mogą różnić się od moich propozycji, jednak zawsze dążymy do tego samego by uzyskać wiarygodne i czytelne testy jednostkowe. Zapraszam do zostawiania komentarzy z waszymi dobrymi radami. W następnym wpisie wracam do kontynuacji serii o ciągłej integracji.