Jeżeli w tworzeniu oprogramowania nie stosujecie jeszcze techniki testów jednostkowych, gorąco zachęcam, by to zmienić.
Test jednostkowy to – w skrócie – podprogram, który weryfikuje poprawność działania jednego modułu funkcjonalnego programu lub jego elementu. Najważniejsze cechy dobrego zestawu testów jednostkowych to:
kompletność — wyczerpuje możliwie szeroki zakres przypadków danych wejściowych oraz stanu testowanego modułu, poprawnych i niepoprawnych, a w szczególności wartości skrajnych, leżących na pograniczu poprawności,
szybkość — wykonuje się tak szybko, by uruchomienie testów wszystkich modułów obszernego projektu trwało najwyżej kilka do kilkunastu sekund,
niezależność — jest niezależny od zewnętrznych uwarunkowań, takich jak połączenia sieciowe, bazy danych, zbiory danych, a także możliwie mało zależny od innych modułów funkcjonalnych.
Testy jednostkowe wspomagają tworzenie oprogramowania na wielu etapach:
podczas tworzenia modułu funkcjonalnego pozwalają stwierdzić, czy moduł działa zgodnie z oczekiwaniami lub formalnymi założeniami,
podczas refaktoryzacji weryfikują poprawność działania modułu po wprowadzeniu zmian oraz wykrywają ich ewentualne efekty uboczne,
podczas usuwania błędów pozwalają na odtworzenie błędu, stwierdzenie jego pomyślnego usunięcia bez efektów ubocznych i upewnienie się w przyszłości, że inne zmiany nie spowodują ponownego pojawienia się takiego samego błędu.
Testy jednostkowe nie mogą oczywiście pokrywać wszystkich możliwych wektorów danych wejściowych i stanów modułów funkcjonalnych. Nie byłoby to zresztą użyteczne: wystarczy wyobrazić sobie sumator liczb, który dla wielu miliardów kombinacji wejściowych będzie musiał działać tak samo dobrze, jak dla jednej z nich. Warto jednak przewidywać sytuacje skrajne, takie jak realizowanie działań na granicy dopuszczalnych wartości typu liczbowego lub z celowo błędnymi danymi. Czasem to, że operacja kończy się zgłoszeniem błędu jest przecież pozytywnym wynikiem testu, a błędem jest brak zgłoszenia sytuacji wyjątkowej.
Istnieje wiele technik stosowania testów jednostkowych. Jedną z bardziej popularnych jest TTD (ang. Test-Driven Development), nazywana też – nieco logiczniej – TFD (ang. Tests-First Development). Tworzenie modułu funkcjonalnego w technice TTD realizuje się zgodnie z następującą procedurą:
Określa się zestaw wymagań funkcjonalnych oraz zgodne z nimi API modułu, na przykład w postaci interfejsu klasy.
Przygotowuje się zbiór testów pokrywających możliwie szeroko przypadki poprawne, niepoprawne i skrajne.
Tworzy się „pustą” implementację interfejsów, wystarczającą by program (wraz z testami) skompilował się poprawnie, lecz nie realizującą żadnych czynności. Większość testów oczywiście zakończy się w tym momencie niepowodzeniem, lecz niekoniecznie wszystkie: niektóre „puste” wyniki operacji mogą być dla niektórych danych całkowicie poprawne.
Implementuje się poszczególne elementy funkcjonalne tak, by zapewnić pozytywne wyniki testów. W miarę postępowania implementacji zbiór testów można rozszerzać tak, by pokrywać przypadki skrajne odkrywane w implementacji.
W momencie, gdy wszystkie testy jednostkowe kończą się sukcesem, można założyć, że moduł funkcjonalny jest zaimplementowany poprawnie i przejść do jego wdrażania oraz testowania jego integracji z innymi modułami.
W teorii TFD wygląda wspaniale. W praktyce jednak potrafi być koszmarnie niepraktyczne:
ilość kodu związanego z testami znacząco przewyższa ilość kodu testowanego,
uzyskanie wystarczającego pokrycia charakterystycznych przypadków testowych jest bardzo trudne,
istnienie obszernego zbioru testów zniechęca do przeprowadzania poważnej refaktoryzacji modułu, związanej ze zmianą API lub algorytmów.
Alternatywne, bardziej praktyczne podejścia do stosowania testów jednostkowych przedstawię w kolejnych wpisach z tej serii.