Damian Nowak is a CEO at Virtkick. He's a Ruby coder, an Arch Linux hacker, and drinks good beer.
Dzisiaj klient zgłosił mi problem z jedną z jego starych stron. Problem polegał na tym, że zawartość strony wyświetla się trzy razy. Rzeczywiście, kilka dni wcześniej majstrowałem trochę w kodzie kontrolera i widoku, aby ulepszyć generowanie meta-danych pod SEO i przy okazji popsułem inne miejsce.
Ta sytuacja zmotywowała mnie, aby napisać trochę o testach jednostkowych. Stosuję je w codziennej pracy przy niektórych projektach w Javie i PHP - i często ratują mi skórę.
Testy jednostkowe można porównać z testami wybranej partii produktów z fabryki. Testowane są pod kątem zgodności z założeniami - długością, szerokością, wagą i położeniu nadruków. Testowane jest również działanie tych produktów w jakimś symulowanym środowisku.
W inżynierii oprogramowania wygląda to analogicznie. Bierzemy naszą klasę, wykonujemy kilka operacji na niej i sprawdzamy, czy rezultat jest zgodny z naszymi oczekiwaniami.
Praca z kodem to dość skomplikowany proces. Implementacja nowych funkcjonalności nie tylko wymaga tworzenia nowych klas, ale i częstokroć modyfikowania obecnych. O ile niedziałająca nowa klasa nie jest żadnym problemem, to popsucie istniejącej klasy niesie z sobą już poważne konsekwencje. Inni komponenty programu mogą korzystać z tej klasy i nagle cała aplikacja się sypie. W dodatku, błąd może się ujawniać tylko dla specyficznych przypadków i wtedy znalezienie takiego błędu jest naprawdę kosztowne. Testy jednostkowe mają zabezpieczyć programistę przed przypadkowym popsuciem czegoś innego. W przypadku negatywnego wyniku testów jednostkowych, mamy podane na tacy miejsce występowania błędu. Bez testów błędu trzeba samemu szukać.
Każdy test powinien testować niewielki fragment systemu, aby w przypadku jego negatywnego wyniku można było łatwo znaleźć miejsce błędu. Ważne jest, aby test wykonywał się szybko. Tylko wtedy programista będzie miał ochotę uruchamiać test co kilka minut. Test, który wykonuje się długo, np. pół godziny nie będzie chętnie uruchamiany.
Na samym początku sprawdzamy przede wszystkim poprawność wyniku. Weźmy za przykład koszyk z zakupami. Przykładowymi testami koszyka mogą być:
Każdy programista pewnie przyzna, że najwięcej błędów daje o sobie znać, gdy zachodzą tzw. warunki brzegowe. Jednym słowem, gdy kod zachowuje się nietypowo, inaczej niż zawsze. Typowym przykładem jest dzielenie przez zero.
Odsyłam do rozdziału czwartego książki „JUnit. Pragramtyczne testy jednostkowe w Javie”. Tak się składa, że Helion udostępnił ten rozdział za darmo w Internecie w formacie PDF. (P.S. Nie polecam tej książki, nie jest warta swojej ceny, omawia starą wersję JUnit oraz nie pokazuje dobrych praktyk pisania testów jednostkowych)
Skorzystam z listy przedstawionej w tej książce, aby omówić najważniejsze aspekty testowania warunków brzegowych.
Często nasze metody oczekują danych w ściśle określonym formacie. Np. adresy e-mail muszą być zgodne ze swoim RFC, a liczby dziesiętne zapisane jako string powinny używać kropki jako separatora dziesiętnego.
Jako przykład wezmę znajdowanie najmniejszej liczby całkowitej w liście. Warto przetestować, czy metoda zwraca poprawny wynik, jeśli najmniejsza liczba znajduje się na początku listy, gdzieś w środku oraz na końcu. Może się bowiem okazać, że w wyniku naszej pomyłki metoda, która miała zwracać najmniejszą liczbę na liście zwraca jej pierwszy element. Przy sprzyjających warunkach nasza metoda będzie działała, tj. gdy lista będzie posortowana. Wszystko popsuje się, gdy lista nie będzie uporządkowana. Test jednostkowy nam w tym pomoże i ostrzeże o nieodpowiednim działaniu.
Jasne jest, że metoda konwertująca stopnie w skali Kelvina na Celsjusza może przyjmować wartości większe lub równe zeru. Przetestujmy więc działanie metody dla skrajnego przypadku - zera bezwzględnego oraz wartości ujemnej. W przypadku zera bezwzględnego zwyczajnie sprawdzamy, czy nie pomyliliśmy się przy implementacji, np. dając nieodpowiednie równanie (>= zamiast >). Wartość ujemną sprawdzamy, aby upewnić się, że rzucany jest odpowiedni wyjątek.
Wyobraźmy sobie malarza. Malarz może malować ściany tylko, jeśli jest w odpowiednim pokoju oraz ma pędzel w ręce. ;-) Przetestujmy więc, czy malarz zachowuje się prawidłowo, tj. nie maluje, jeśli nie ma pędzla w ręce.
NullPointerException to chyba jeden z najczęściej pojawiających się błędów w aplikacjach Javy. Łatwo jest się ich pozbyć, jeśli przygotujemy odpowiedni wachlarz testów. Warto przetestować zachowanie metod, próbując dać null jako parametr, gdzie tylko się da.
Czy obiekt zachowuje się poprawnie, jeśli korzysta z niego wiele wątków naraz? Co się stanie, jeśli wywołam metody w nieodpowiedniej kolejności?
Dla Javy istnieje kilka narzędzi do testowania, najpopularniejszym jest JUnit - każde szanujące się IDE obsługuje jego testy. Można więc wydajnie i co chwilkę uruchamiać test i oczekiwać zielonego światełka. ;-) Szczególnie polecam JUnit w wersji 4 z powodu jego uproszczeń.
PHPUnit to narzędzie do testowania kodu PHP. Nie widziałem integracji IDE z PHPUnitem, jednak nie jest to aż taki problem. W Eclipse PDT zdefiniowałem zewnętrzne narzędzie, które uruchamia testy PHPUnit w konsoli. Nie zapomniałem też o skrócie klawiszowym dla tej akcji.
Niedługo postaram się napisać coś o pisaniu testów zgodnie z szablonem „given-when-then”, które zwiększają jakość oraz czytelność testu. Należy bowiem pamiętać, że test powinien być takiej samej dobrej jakości, jak kod testowej aplikacji.