Damian Nowak is a CEO at Virtkick. He's a Ruby coder, an Arch Linux hacker, and drinks good beer.

Testy jednostkowe – czym są, po co testować i jak?September 2009

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ę.

Czym są testy jednostkowe?

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.

Po co testować?

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ć.

Jak testować?

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.

Poprawność wyniku

Na samym początku sprawdzamy przede wszystkim poprawność wyniku. Weźmy za przykład koszyk z zakupami. Przykładowymi testami koszyka mogą być:

  1. Do pustego koszyka wkładam jeden test jednostkowy i jedną klasę. Potem czyszczę zawartość koszyka. Czy ilość kodu w koszyku jest równa zeru?
  2. Do pustego koszyka wkładam jedną test jednostkowy i dwie klasy. Czy w koszyku znajduje się jeden test jednostkowy i dwie klasy? Testujemy od razu metodę zliczającą. Czy łączna ilość kodu w koszyku wynosi dwa? Czy łączna ilość sztuk kodu w koszyku wynosi trzy?
  3. Do pustego koszyka wkładam jeden test jednostkowy. Potem wkładam jeszcze dwa testy. Czy łączna ilość testów jednostkowych to trzy?
  4. Do pustego koszyka wkładam dwa takie same testy jednostkowe. Potem zmieniam ilość testów na jeden. Czy w koszyku znajduje się tylko jeden test?

Warunki brzegowe

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.

Zgodność

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.

Uporządkowanie

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.

Zakres

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.

Warunki początkowe

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.

Istnienie

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.

Czas

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?

Czym testować?

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.

Zakończenie

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.

Damian Nowak
CEO & Ruby Developer