Najpoważniejszym wyzwaniem stojącym przed programistą jest sprawienie, by napisany kod poprawnie realizował wymagania klienta. Jedną z technik, które pozwalają utrzymać w ryzach jakość powstającego oprogramowania jest TDD. Jej właśnie zostało poświęcone czwarte spotkanie Koła Naukowego FullStack, które odbyło się 19 grudnia na Wydziale Automatyki, Elektroniki i Informatyki Politechniki Śląskiej w Gliwicach. Poniższy tekst stanowi relację z tego wydarzenia.
Zasady testowania oprogramowania zmieniały się na przestrzeni lat. Najpierw programy były testowane głównie przez samych twórców oraz użytkowników końcowych. Wraz z rozwojem oprogramowania komercyjnego do procesu zostali włączeni profesjonalni testerzy, których zadaniem było zapewnienie wysokiej jakości programów, choćby było to okupione gnębieniem programistów. W końcu, by zdjąć z testerów najbardziej monotonne zadania i umożliwić sprawdzanie w ciągu paru minut, czy znane wcześniej błędy nie powróciły wraz z nowo wprowadzonymi zmianami, zaczęto stosować testy automatyczne.
Obecnie do testów automatycznych zaliczamy jednostkowe, integracyjne, całościowe (ang. end-to-end) oraz wiele drobniejszych kategorii. Istnieje też wiele metodologii decydujących o tym, na jakim etapie rozwoju wprowadzać testy automatyczne i w jaki sposób je rozwijać.
Jedną z ciekawszych oraz modnych obecnie technik jest TDD, nazywana też TFD. Zadania przedstawienia jej szerszej publiczności podjął się Artur Dębski podczas czwartego, ostatniego w tym roku spotkania Koła Naukowego FullStack, działającego na Wydziale Automatyki, Elektroniki i Informatyki Politechniki Śląskiej w Gliwicach we współpracy z firmą Gorrion Software House. Jego prezentacja, zatytułowana „Test Driven Development: jak testować, żeby nie zwariować (a przynajmniej nie za bardzo)” miała na celu pokazanie zalet i wad techniki oraz zademonstrowanie jej użycia w praktyce.
Prelegent rozpoczął od wyjaśnienia podstaw techniki. Cały proces rozwoju nowego elementu programu musi zacząć się od zapisania testów dotyczących publicznego API tego modułu, zanim jeszcze powstanie choć jeden wiersz tekstu źródłowego testowanej klasy lub metody. Bezpośrednią korzyścią z takiego podejścia jest obecność testów, których w przypadku wcześniejszego implementowania funkcjonalności nie chce się często pisać. Chodzi jednak o dużo więcej: zapisanie testów, a więc kodu wykorzystującego nowo tworzony moduł, przed implementowaniem API tego modułu powoduje, że programista jest zmuszony do przemyślenia, czy to API faktycznie spełni wymagania użytkowników. Jeżeli korzystanie z modułu jest zbyt trudne, dowiemy się o tym już podczas pisania testów, a nie dopiero przy produkcyjnym wykorzystaniu kodu. Z tego powodu skrót „TDD” rozwija się czasem również jako Test-Driven Design.
Ponadto, powstająca implementacja będzie od razu napisana z myślą o możliwości stosowania testów automatycznych. Dodawanie testów do istniejącego kodu bardzo często wiąże się z koniecznością refaktoryzowania go i wymaga dużego (lub bardzo dużego) nakładu czasu. Napisanie testów w pierwszej kolejności oznacza w takim przypadku oszczędność czasu, gdyż tworzona implementacja będzie od razu możliwa do przetestowania.
Z obecności testów wynikają oczywiste korzyści. Najważniejszą jest ochrona przed regresją: jeżeli cokolwiek zmieni istotne z punktu widzenia programu zachowanie testowanego elementu, możemy wykryć i usunąć usterkę zanim trafi ona do klientów. W efekcie wzrasta poziom zaufania do kodu, a programiści nie boją się wprowadzać daleko idących zmian w implementacji. Testy stanowią też najlepszą możliwą dokumentację danego modułu funkcjonalnego, gdyż stanowią zapis kontraktów wejściowych i wyjściowych poszczególnych podprogramów. Ostatecznie należy je zatem uznać za element dający zysk odkładający się w czasie: choć na początku będziemy uważać, że pisanie testów zmniejsza naszą produktywność i wydłuża czas od pomysłu do produktu, na etapie utrzymania i refaktoryzacji kodu spowodują one sporą oszczędność czasu, nerwów, a być może również pieniędzy.
Testy automatyczne powinny być szybkie, aby programistów nie korciło ich pomijanie. Najwolniejsze fragmenty kodu, nieistotne przy testowaniu danego modułu funkcjonalnego, powinny być zastępowane imitacjami (ang. mocks) tak, aby operacje trwające setki lub tysiące milisekund realizować w ciągu pojedynczych mikrosekund. Stosując imitacje trzeba jednak stale zwracać uwagę, by nie zacząć testować tych imitacji zamiast produkcyjnego kodu programu.
Podczas pisania testów należy skupić się nie na implementacji, lecz na zachowaniu testowanych klas i podprogramów. Prelegent zdecydowanie sprzeciwił się wprowadzaniu do testów założeń co do stanu obiektów, a także odradził testowanie prywatnych metod klas.
Poszczególne testy powinny składać się z trzech części, wspólnie realizujących zasadę „trzech A” (uważni Czytelnicy mogą zauważyć podobieństwo tego podziału do given-when-then stosowanego w technice BDD i opisanego w relacji z zeszłorocznej konferencji SpreadIT):
Ponadto, wszystkie testy powinny spełniać założenia F.I.R.S.T., opisane przez Roberta Martina w książce „Clean code”:
Jako źródło wiedzy teoretycznej i praktycznej prelegent wymienił dwie książki:
Druga część spotkania poświęcona była praktycznej prezentacji zastosowania TDD. Prelegent postawił sobie za zadanie zapisanie w języku JavaScript metody greet()
spełniającej wymagania Greeting Kata. Kolejne, coraz bardziej abstrakcyjne wymagania opisane w dokumencie prowadziły do powstawania kolejnych testów, a bezpośrednio po nich — do wprowadzania refaktoryzacji i rozszerzeń kodu. W paru przypadkach zmiany faktycznie psuły wcześniej działającą funkcjonalność, jednak obecność testów pozwalała szybko to wykryć i wprowadzić odpowiednie poprawki.
Spotkanie zorganizowane przez Koło Naukowe FullStack było świetną okazją, by osoby nie zaznajomione jeszcze z techniką testów jednostkowych lub z jej zastosowaniem w metodologii TDD dostrzegły wartość dodawaną do projektów informatycznych przez testy. Artur Dębski doskonale podzielił przyznany mu czas pomiędzy teorię i praktykę, najpierw słownie uzasadniając potrzebę testowania w ogóle, a za pomocą TDD w szczególności, a później — demonstrując na żywo jak opisane reguły stosuje się w codziennej pracy programisty. Szczególne brawa należą się mu za odwagę związaną z używaniem sugerowanego na spotkaniach Koła Naukowego języka JavaScript, nie będącego jego podstawowym językiem.
Do wystąpienia miałem jedynie następujące, drobne uwagi, dotyczące głównie braku pewnych informacji związanego z ograniczonym czasem spotkania:
To, jak istotnym tematem jest testowanie jednostkowe oraz technika TDD widać po częstości, z jaką pojawia się on na wystąpieniach:
Przy okazji, programistom Java chcącym nauczyć się stosowania techniki testów jednostkowych, w tym zgodnie z metodologiami TDD/TFD oraz TAD, polecam książkę Testowanie aplikacji Java za pomocą JUnit mojego autorstwa.