Red -> green -> refactor-> red again…
ten schemat, czy w zasadzie cykl działania w metodyce TDD znany jest prawie każdemu. W obecnych czasach pisanie testów jednostkowych jest nieodłącznym, a często nawet pierwszym elementem pracy – jeśli mamy zamiar wytwarzać oprogramowanie w zwinnych metodykach, takich jak Agile czy Scrum.
Ale zatrzymajmy się na chwilę i zacznijmy od początku. W tym poście postaram się odpowiedzieć na pytania: co to właściwie są testy jednostkowe? Kiedy należy, a kiedy nie należy ich pisać? Jak pisać dobre testy? Czy pokryty w sporej części testami kod zapewnia nam bezbłędne działanie systemu czy też aplikacji? Zaczynajmy!
1. Testy jednostkowe – krótka definicja oraz zastosowanie jej w praktyce
Cytując za Wikipedią, „Test jednostkowy (ang. unit test) – metoda testowania tworzonego oprogramowania poprzez wykonywanie testów weryfikujących poprawność działania pojedynczych elementów (jednostek) programu […]. Testowany fragment programu poddawany jest testowi, który wykonuje go i porównuje […] z oczekiwanymi wynikami – tak pozytywnymi, jak i negatywnymi […]. ”
Ta definicja jest w prawdzie dość prosta, ale dokładnie opisuje, czemu służyć mają testy jednostkowe. Zagłębiając się jednak w artykuł na Wiki nieco dalej, okazuje się, że pewne rzeczy mogą być niejasne – stąd wygodniejszy może być „praktyczna” definicja testów jednostkowych, zaczerpnięta od Macieja Aniserowicza. Otóż, test jednostkowy jest to kod wykonujący inny kod w kontrolowanych warunkach. Kontrolowanych, czyli jakich? – może ktoś przytomnie zapytać. Ano, takich gdzie autor testu dostarcza input (dane wejściowe) oraz oczekiwany output (wyjście) – a test wykonuje pewne instrukcje w celu sprawdzenia, czy owy output jest zgodny z tym, czego oczekuje autor testu. Dobrą praktyką jest to, aby w miarę możliwości jeden test badał jedną ścieżkę wykonania jednej metody.
2. Czy testy to tylko poprawnie działający kod? Of course not!
Dobrze napisane testy mają również pozytywny wpływ na design aplikacji. Nie mówimy tutaj o „wyglądzie zewnętrznym” czyli UI, ale raczej o architekturze. Przecież aby komponent mógł być testowany, powinien udostępniać odpowiednie API, które to jest wykorzystywane również przez pozostałe komponenty systemu. Stosując dodatkowo metodykę TDD, piszemy najpierw test, a później możliwe prosty kod, który ten test spełni. Potrzebna do tego jest chwila przemyślenia, stąd jest duża szansa na to, że unikniemy masy zbędnego kodu na produkcji.
Testy wyręczają nas również w każdorazowym uruchamianiu danej aplikacji celem „przeklikania” się przez interfejs użytkownika i sprawdzenia, czy wszystko działa jak należy. Jest to niewątpliwie duża oszczędność czasu, oczywiście pod warunkiem, że testy są napisane dobrze – w innym wypadku, test sprawdzi tylko to, czy kod działa zgodnie z założeniami autora testu, a nie poprawnie pod względem funkcjonalności jako część większego ekosystemu.
Z czasem, gdy nasze testy są już „dobre” (tak, wiem, że jest to pojęcie względne) – mogą służyć jako dokumentacja projektu. Są czytelne dla innych programistów, jasno wyrażają intencje autora (jeśli mają dobre, opisowe nazwy) oraz są zsynchronizowane z kodem, w przeciwieństwie do komentarzy, które aktualne są tylko w momencie ich pisania. Dodatkowo dobre testy są świetnym źródłem wiedzy dla nowego członka zespołu, który musi w miarę sprawnie zaznajomić się z działaniem systemu, bądź aplikacji.
3. Kiedy pisać testy?
Chciałoby się powiedzieć – zawsze. Ale co to znaczy? Otóż główne możliwości są w zasadzie 3: przed napisaniem właściwego kodu (tak, tak – TDD), przy bugfixach oraz przy okazji refactoringu.
Punkt pierwszy zdaje się być jasny – działając zgodnie z zasadami TDD, zaczynamy od chwili zadumy nad tym, co tak naprawdę chcemy osiągnąć, następnie przemyślenie scenariusza testowego, stworzenie właściwego kodu testu, napisanie kodu programu spełniającego tenże test, refactoring itd. Działanie takie może być jednak problematyczne dla początkujących programistów, dopiero wkraczających w świat tworzenia oprogramowania.
Bugfix – warto przed usunięciem danego buga, oprócz zerknięcia w kod, stworzyć test, który wykaże tenże błąd. Z jednej strony pozwoli nam to faktycznie wykazać, że w tym i tym miejscu błąd występuje, a z drugiej – wiemy, co go powoduje, oraz gdzie jest punkt zaczepienia do naprawy. W końcu, raz napisany test pozwoli nam się zabezpieczyć przed tym samym bugiem w przyszłości! Skupiając się na istocie problemu mamy spore szanse na zidentyfikowanie dodatkowych problemów z danym kawałkiem kodu – same zalety, chciałoby się rzec 🙂
Refactoring – podczas prac porządkowych nad naszym „spaghetti-code” warto zastanowić się, czy nie można przy okazji „przepisywania” kodu na nowo, otestować go. Zmiana designu w całym tym „bajzlu” może mieć pozytywny wpływ na późniejsze utrzymanie całego systemu czy aplikacji. Dopisanie w przyszłości do takiego zrefaktoryzowanego i otestowanego kawałka kodu nowej funkcjonalności również powinno przyjść nam nieco łatwiej.
4. Dobre testy, czyli jakie?
Przede wszystkim dobrze nazwane! Istnieje kilka dobrych praktyk mówiących o tym, jak należy tworzyć nazwy testów, natomiast warto przemyśleć to „zdroworozsądkowo” i zamiast zawsze ślepo iść za standardami, postawić na jakość i czytelność kodu! W końcu standard jest po to, aby ułatwić, a nie utrudnić nam życie…
Wąż zamiast wielbłąda
W długich nazwach testów (powyżej 4 słów) warto stosować notację snake_case zamiast CamelCase. Oddzielenie słów za pomocą podkreślenia ma pozytywny wpływ na czytelność, a w przypadku NAPRAWDĘ długich nazw (posiadających nawet kilkanaście wyrazów) – jest niemal niezbędne.
Twierdzenia zamiast pytań
Nazwa testu powinna stwierdzać to, co stanie się po jego uruchomieniu, a nie zmuszać programistę do przemyśleń na ten temat, dopiero podczas lektury kodu samego testu. Zamiast nazwy „what_if_user_enters_invalid_credentials_twice” zdecydowanie lepiej użyć „ throws_error_if_user_enters_invalid_credentials_twice” lub nawet “redirects_to_password_reminder_page_if_user_enters_invalid_credentials_twice”. Nie bójmy się długich nazw!
Nie stosuj numeracji testów
Zamiast tego przemyśl i przepisz nazwę testu tak, aby jednoznacznie określał to, co dany test ma sprawdzić.
O pozostałych „technikach” pisania dobrych testów znajdziesz wiele dobrych pozycji książkowych, i tam też Cię odsyłam – gdyż temat znacząco wykracza poza ramy tego posta 🙂
5. Gdzie koder nie może, tam mocka pośle?
Mocki, czyli w „kulawym” tłumaczeniu na polski „zaślepki” – służą do symulowania zachowania środowiska zewnętrznego względem testowanej klasy. Mówiąc nieco ogólniej, używamy ich wszędzie tam, gdzie testowana metoda zależna jest od innego czynnika, jak inna klasa bądź baza danych. Test bowiem nie powinien sprawdzać (ani być zależny) od tego, czy w danej chwili mamy połączenie z bazą danych, aby pobrać z niej konkretne dane. Do mockowania najlepiej jest używać frameworków, których dla różnych języków programowania jest NAPRAWDĘ całą masa. Przykładem niezłego frameworka dla języka Java jest Mockito, a dla technologii .NETowych – FakeItEasy, czy Moq. Dobry framework można poznać po tym, że (po zajrzeniu w dokumentację) wykorzystuje się go intuicyjnie. Warto dodać, że w przypadku Mockito próg wejścia, zwłaszcza dla nowych użytkowników, jest całkiem niski.
6. Czy aplikacja otestowana to aplikacja bezbłędna?
Tu odpowiedź może być tylko jedna, N I E!
Samo napisanie testów i to, że „przechodzą” nie sprawi w magiczny sposób, że nasz kod będzie „odporny na inteligencję”. Dobry test powinien sprawdzać, czy dla danych argumentów metoda zwróci taki, a nie inny wynik, obliczony przez nas niezależnie od samego programu. SPOSÓB obliczania tego wyniku nie powinien być przedmiotem badanym przez ten test jednostkowy. Zły test sprawdzi tylko, czy kod działa tak, jak chce tego programista, a nie tak, jak powinien!
Podsumowując,
temat testów jednostkowych to oczywiście nie tylko poruszone powyżej kwestie, ale wiele, wiele więcej. Jeśli jesteś ciekawy tej wiedzy „here and now”, polecam zajrzeć na blog Macieja Aniserowicza, gdzie stworzył swojego czasu obszerny cykl traktujący o testach właśnie, a oprócz tego napisał wiele ciekawych artykułów na ten temat. Ja w miarę możliwości postaram się kontynuować ten mini-cykl na swoim blogu, szerzej opisując między innymi TDD – także stay tuned 🙂