Skuteczne testowanie własnego kodu, jest rownoważne z samym programowaniem. Tekst: dziwnie, u mnie działa, raczej nie obroni źle zaimplementowanej funkcjonalności. Co zrobić, żeby testowanie kodu nie bylo udręką, a rzetelnym źrodłem informacji o kodzie?
Jeśli dopiero zaczynasz swoją przygodę z programowaniem sprawdź naszą lekcję javy odnośnie testów jednostkowych. Uwaga, we wpisie będę podawała przykłady bibliotek javowych, ale na pewno znajdziecie ich odpowiedniki w swoich technologiach.
W największym skrócie, stosując angielski skrót: testowanie kodu, to nic innego niz AAA: Arrange, Act and Assert. Po pierwsze, musimy dobrze przygotować i zainicjować testowany fragment kodu (jak i przyjmowane przez niego parametry i dane testowe), po drugie musimy w poprawny sposób wywołać testowaną metodę i w końcu musimy zapewnić odpowienie sprawdzenie. O tym, jak to działa w praktyce przeczytasz poniżej.
#0 Postawa
Na początku, jako programista, musisz uświadomić sobie, że testy twojego kodu są nieodzownym elementem pracy deweloperskiej. Nie zrobi za Ciebie tego stażysta, junior, tester automatyczny, czy nawet mama :). Stąd też świetną metodą na pisanie testów do swojego kodu, jest test driven development (o którym więcej przeczytasz tutaj). To podejście, nie tylko zmusza do pisania testów, ale, a może przede wszystkim narzuca pisanie ich w sposób wartościowy dla twojego kodu. Zaczynając od testów, możesz nie tylko zapewnić dobre testowanie, ale też przejść przez wszystkie możliwe ścieżki, czy, po prostu znaleźć odpowiednie rozwiązanie problemu. Różni developerzy różnie do testów podchodzą. Dla jednych będzie to sposób na zapewnienie bezbłędności czy wyszukanie bugów, dla innych, sposób na refaktor i lepsze przemyślenie implementacji, albo wręcz gwarancja, że “potomni” będą mogli kontynuować pracę nad projektem. Tak naprawdę, każdą z tych rzeczy warto mieć w głowie gdy testujemy swój kod. Bo testy to tak naprawdę druga noga programowania i bez nich, nie można mówić o ukończonym zadaniu.
#1 Testy jednostkowe
Tak, dokładnie takie – jednostkowe, czyli takie, które testują najmniejsze elementy /jednostki programu. Taka jednostka powinna być niezależna od innych elementów aplikacji, a każdy test niezależny od siebie (kolejność ich wykonywania nie powinna grać roli).Jak można zapewnić takie warunki? Po pierwsze mockując inne elementy programu, które są wywoływane przez testowany kod. W javie możesz skorzystać z bibliotek takich jak Mockito (tutaj nasz wpis, z praktycznym wykorzystaniem tej biblioteki), JMockit, EasyMock czy PowerMock.
Po drugie każdy test powinien być niezależną jednostką, działającą w tych samych warunkach. Wszystkie zmienne, czy stany aplikacji powinny być “czyszczone”. Jeśli pewne czynności wykonujemy dla każdego testu, używając JUnit możemy skorzystać z anotacji @Before/After czy @BeforeAll/@AfterAll (gdy chodzi o jednorazowe przygotowanie do testów np. zainicjowanie serwisu). Same testy nie powinny na siebie wzajemnie wpływać.
#2 Znajdź wszystkie ścieżki
Czasami w swoim bądź cudzym kodzie możemy znaleźć taki kwiatek: jeden test, napisany tak, że wszystkie wymagane parametry są ustawione w sposób prawidłowy, wszystkie walidacje i asercje przechodzą, otrzymujemy oczekiwany wyniki. I w takim teście nie ma nic złego — póki obok niego są też te obsługujące ścieżki niepowodzeń. Nasze testy powinny pokrywać wszystkie możliwe scenariusze, dlatego ważne jest, by dobrze zrozumieć zadany problem biznesowy np. organizując spotkanie Three Amigos. Nie sztuka napisać kod, który działa w ściśle określonych, oczekiwanych warunkach. Niemniej, jeśli znajduje się w nim walidacja, albo dla określonych parametrów przewidywane jest inne zachowanie — to wszystko powinno zostać uwzględnione w naszych testach. Podobnie warto sprawdzić warunki brzegowe. A wszystko po to, by dostarczyć rozwiązanie odpowiadające w pełni potrzebą uczestników.
#3 Używaj danych zbliżonych do prawdziwych
Używanie pliku z jedną linijką danych, podczas gdy użytkownik na co dzień chce korzystać z takich, gdzie jest ich tysiące, może być ryzykowne. Od samego mechanizmu odczytu, po np. walidację czy zwracanie wyniku — dużo rzeczy może działać inaczej, niż byśmy chcieli. Może to naiwny przykład, ale dobrze ilustrujący zasadę: zawsze lepiej stosować dane, które przypominają te od użytkownika.
#4 Sprawdzaj uczciwie
Sprawdzenie, czy otrzymany wynik nie jest nullem to za mało :) Warto tworzyć jak najdokładniejsze sprawdzenia w naszym kodzie, tak by nic nie umknęło — czy pola są odpowiednio ustawione, czy zwracany obiekt przechodzi walidację (jeśli zwracamy taki sam typ jak przyjmujemy), czy nie ma pól-kolekcji, które są nullami itd
#5 Nie trać czasu na oczywistości
Nie testuj kodu bibliotek, nie sprawdzaj oczywistości. Boilerplate code, który nie ma logiki biznesowej nie musi być testowany. Jeśli jednak pokusiłeś się o własną implementacje metody Equals (a nie skorzystałeś np. z Lombowej anotacji @EqualsAndHashCode), to musisz ją sprawdzić. Akurat w tym specyficznym przypadku możemy podsunąć Ci fajną bibliotekę, a mianowicie: EqualsVerifier, który znacznie ułatwi to zadanie.
#6 Code Coverage
To często zarówno błogosławieństwo, jak i przekleństwo w projekcie. Dobrą praktyką jest mierzenie pokrycia kodu testami, a także zasada, że nigdy dodając nowy kod jej nie obniżamy. Złą zasadą jest pisanie testów pod zwiększenie tego wskaźnika, bez słusznych asercji, zastanowienia, pokrycia całej funkcjonalności, czy dopisywanie testów wspomnianego wcześniej kodu, którego działanie jest oczywiste. Dlatego jeśli w swoim projekcie zdecydujecie się na mierzenie tego wskaźnika (a można tutaj skorzystać chociażby z Jacoco, czy innych wbudowanych w IDE narzędzi), warto dodać do niego zdrowy rozsądek i nie testować “na siłę” coby słupki nie spadły. Testuj to, co ma sens, ale pisz testy wyczerpujące wszystkie ścieżki w kodzie.
O tym, jak nie należy podchodzić do Code Coverage, poczytasz także na blogu dev.to .
#7 Nie tylko jednostkowe
Warto pamiętać, że oprócz poprawnego działania pojedynczych fragmentów naszego kodu, liczy się też to jak będzie to działało w ramach całego komponentu, a także, czy poza poprawnością mamy też oczekiwaną wydajność. Dlatego, do testów jednostkowych warto dodać testy integracyjne, które pozwolą przekrojowo sprawdzić poprawność działania aplikacji (a także udokumentować jej działanie) i starają się jak najlepiej oddać jej normalne używanie, konfigurację i środowisko. Do tego, warto pokusić się o przeprowadzenie testów wydajności, lub skonfigurowanie narzędzi do zbierania metryk. Jeśli w pewnym momencie aplikacja “zwolni”, będziecie o tym wiedzieli szybciej, niż użytkownik, bo już w fazie developmentu.
#8 Testy jako dokumentacja kodu
Dobrze napisane testy, są jak dobry podręcznik dla Developera — tłumaczą co testują, jakimi danymi, jaki jest oczekiwany wynik, jakie możliwe ścieżki interakcji. Im są bardziej uporządkowane (Given — When — Then), im lepsze mają nazwy, dokładniej odzwierciedlają prawdziwe działanie aplikacji tym lepiej mogą zastępować dokumentacje, komentarze czy sesje z nowymi osobami w projekcie. Testy to też kod, więc jak najbardziej podlegają zasadom Clean Code’u, samo wyjaśniające się zmienne, kod podzielony na bloki, wyłączanie stałych, czy metod jak najbardziej tutaj obowiązuje ;)
W pisaniu testów do swojego kodu, tak jak w samym pisaniu kodu — nie można iść na skróty. Dobrze przetestowany kod, to kod, który spełnia założenia biznesowe, jest czytelny i prosty (bo TDD zachęca nas do refaktoru), dobre opisany (właśnie przez testy), oraz sprawdzony nie tylko w izolowany sposób poprzez testy jednostkowe, ale też przekrojow, testami integracyjnymi, jak i monitorowany pod względem wydajności. Taki kod powinien być łatwiejszy w przekazaniu koledze, gdy jedziesz na urlop, a także nie powinien powodować bólu głowy, gdy dostajesz go w spadku w starym firmowym projekcie.