TDD, czyli dziadek do orzechów
  • 1 081 views

Znana, programistom, stara prawda mówi, że każdy system przetestuje się sam - po wdrożeniu. Zupełnie jak w magicznym domu z tekstu piosenki Hanny Banaszak, gdzie „same zmyślają się historie” i „sam się rozgryza orzech”. Niestety, projektowanie, kodowanie, weryfikacja, poprawki i wdrożenie systemu IT, to wypracowany przez lata cykl ewolucji struktury aplikacji , który wciąż pozostaje niedoceniany.

Udostępnij!

Traktując ten cykl wytwórczy tylko jako utarty, wyświechtany slogan budzimy licho a zegar projektu nieubłaganie zaczyna odmierzać nadgodziny. Niezrozumienie cyklu skutkuje nieświadomym projektowaniem, kodowaniem, testowaniem, co staje się jednym z poważniejszych czynników psujących statystyki projektu, obniżenia poziomu jakości produktu końcowego, opóźnień w realizacji zadań i co najgorsza – zabijających optymizm i satysfakcję z wykonywanej pracy.

Ten negatywny obraz potęguje fakt braku świadomości wspomnianych zasad cyklu wytwórczego wśród pozostałych członków zespołu – także tych nie związanych bezpośrednio z procesem kodowania a mających wpływ na poziom jakości i terminowość realizowanych zadań, czyniąc ich także odpowiedzialnymi i za wyeliminowanie problemu.

Z perspektywy programisty mogę powiedzieć, że w natłoku codziennych zadań, jakie mamy do wykonania, pewne rzeczy, zwłaszcza te nieprzewidziane, wykonujemy automatycznie, przyjmując je jako poprawne i kompletne. Może warto to zmienić, aby potem nie okazało się, że gros naszego czasu zostało zmarnowane.

Jak często przerabiałeś stary fragment kodu uznany wcześniej za ukończony i poprawny? Ile razy po zaimplementowaniu rozwiązania nie spodobała ci się jego architektura i z tego powodu musiałeś ponownie napisać większą część kodu? Jak często kończyłeś programowanie z przeświadczeniem, że coś pominąłeś, ale nie byłeś w stanie zdefiniować co konkretnie? Ile razy kończyłeś dzień pracy z myślą, że przez drobne błędy, które poprawiałeś pół dnia, nie skończyłeś zadania? Odpowiedź brzmi ..”zbyt często”.

Szacując czas kodowania, programista nastawiony pesymistycznie popełni błąd przeszacowania a nastawiony optymistycznie – niedoszacowania. Niezależnie od tego, jeśli nie wiedzą oni jaką część czasu kodowania pochłania reagowanie na bieżące problemy, obydwaj prawdopodobnie nieświadomie zaniżą swoje oceny. W efekcie, pesymistyczny programista i tak nie dotrzyma terminu realizacji zadania a z początku optymistyczny programista powiększy swój błąd tak bardzo, że przy następnych szacowaniach optymizm i uprzednia wiara we własne siły ustąpią miejsca pesymizmowi.

W ten sposób, bez względu na zastosowaną strategię, wszyscy, nieświadomie zmierzamy drogą, która kolejny raz prowadzi nas do przekroczenia terminu realizacji projektu i co równie ważne, zabija optymizm i satysfakcję z wykonywanej pracy. Nieświadome kodowanie nie stanowi wartości, ponieważ nie podlega kontroli i jako takie, nie może być skutecznie ukierunkowane. W nieświadomej formie służy tylko jednemu – aby doprowadzić kod do postaci wystarczająco dobrej w danej chwili, czyli dającego się uruchomić programu – ale to zbyt mało.

Zadowalającą jakość aplikacji uzyskamy, między innymi, wykrywając luki wymagań i architektury oraz eliminując jak największą liczbę błędów w kodzie najwcześniej jak to tylko możliwe. Z pewnością trudno osiągnąć ten cel w zespołach o niewielkiej świadomości tego w jaki sposób przebiega proces kodowania. Testowanie to nie sprawdzanie, że 2 + 2 = 4.

Jak zaplanować kodowanie? Samego testowania debugerem oczywiście nie zaplanujesz. Nie przewidzisz też momentu, w którym stanie się konieczne zweryfikowanie wcześniejszych założeń i koncepcji. Ale możesz przedefiniować swoje myślenie o projekcie i odpowiednio wcześniej zadać ważne pytania typu: Jaki jest dokładnie format danych wejściowych i wyjściowych? Skąd wziąć zestaw danych testowych, który pozwoli sprawdzić poprawność i stabilność działania modułu dla warunków brzegowych i dla każdej ze ścieżek? Jakie są konkretne wymagania co do wydajności działania?  Jaka jest kolejność wywołań metod kooperujących ze sobą obiektów? Jak upewnić się, że żaden krok nie został pominięty oraz że wybrana architektura nie będzie nadmiernie komplikowała kodu? Skąd wziąć mechanizm, który sprawdzi czy nowo dodany kod nie zdestabilizuje działania istniejącej części systemu? Jak sprawdzić architekturę rozwiązania, które nie zostało jeszcze zaimplementowane?

Na większość z tych pytań odpowiedzi uzyskiwane są w fazie testów. Szkopuł jednak w tym, że faza testów jest ostatnim elementem cyklu wytwórczego, kiedy jest już za późno na poprawę błędów i mleko zostało już rozlane. Jak sobie z tym poradzić?

Z pomocą przychodzi TDD, technika wytwarzania oprogramowania, którą krótko i zwięźle charakteryzują słowa: test-first. Najpierw test, a więc pewność, że architektura którą budujesz przystępując do implementacji funkcjonalności będzie miała odzwierciedlenie w spójnym, przejrzystym i czytelnym kodzie. Dzięki temu będziesz miał zdefiniowaną strukturę danych, warunki brzegowe, oczekiwany poziom wydajności (czas działania) i podstawowy pakiet danych testowych, który łatwo potem rozszerzać, będziesz znał wymagania a także wyeliminujesz luki w wiedzy biznesowej – nowy kod nie da się uruchomić dopóki nie odpowiesz na wszystkie pytania, jakie postawiłeś podczas pisania testu.

Brzmi zachęcająco? Jeśli tak, to kolejny projekt zacznij od pisania testu do funkcjonalności, której jeszcze nie ma. To sprawi, że postawisz sobie i zespołowi pytania, których inaczej byś nie zadał (a już z pewnością nie tak wcześnie) . Odpowiedź na te pytania jest niezbędna do napisania testu. Zanim napiszesz pierwszą linijkę kodu produkcyjnego będziesz miał już jasno zdefiniowane wymagania wydajnościowe, zakres i podstawowy pakiet danych testowych oraz  prawidłową komunikację pomiędzy obiektami i modułami. Co więcej TDD niesie ze sobą także automatyzację uruchamiania testów przy każdym budowaniu.

Podejście test-first wymaga dyscypliny i rzetelności – tego uczy praktyka. Już na wstępie musisz zadać trudne pytania i liczyć się z tym, że nie uzyskasz natychmiastowej odpowiedzi. Odsłonisz luki w wiedzy architektonicznej, biznesowej a także w kontekście organizacji pracy zespołu. Bez ich  uzupełnienia trudno będzie Ci rzetelnie zaimplementować funkcjonalność i uznać zadanie za faktycznie zakończone. Do minimum ograniczysz ryzyko wystąpienia bardzo nieprzyjemnej sytuacji, w której musisz niewiedzą tłumaczyć fakt, że zakończone miesiąc temu zadanie wymaga jeszcze dodatkowych kilku dni pracy, ponieważ w tzw. międzyczasie okazało się że ….

Stawiając pierwsze kroki w TDD trzeba dodatkowo zmierzyć się z własnymi nawykami i uprzedzeniami. Pisząc test do nieistniejącej funkcjonalności wyobrażasz sobie jak chcesz używać obiektów logiki biznesowej. Umieszczasz w konkretnej perspektywie testowej wymyśloną architekturę. Twoje IDE zaczyna krzyczeć ostrzeżeniami i komunikatami o błędach. Nic dziwnego, skoro odwołujesz się do nieistniejącej infrastruktury obiektów. W zamian, jak na dłoni widzisz czy kontrakty na poziomie obiektu i modułu są rzeczywiście takie jak je sobie wyobrażałeś, czy są wystarczająco przejrzyste, czy architektura powiązań pomiędzy obiektami nie będzie komplikowała kodu produkcyjnego. Dodając do tego dobrze dobrany pakiet danych testowych – otrzymasz kompletny test. Nagrodą jest niemal 100% pewność (przecież, bez względu na stosowane sztuczki, zawsze można coś przeoczyć), że już zakończone zadania nie będą w przyszłości wymagały czasochłonnych poprawek.

TDD to przesunięcie paradygmatu z „chyba działa”  i „u mnie działa” do „jestem pewien, że działa wystarczająco dobrze”. Cykl wytwórczy jest jak Ziemia, okrągły  – gdzie ostatni element (faza testów) powadzi do punktu startowego (faza planowania). Każdy podróżnik wie, że trzeba przemierzyć wiele mil, żeby zadać sobie te właściwe pytania i zobaczyć cel podróży. W projekcie opartym o cykl iteracyjny wystarczy na starcie spojrzeć za siebie. Wdrażając TDD zacznij od czegoś niekonwencjonalnego, zacznij od testu – zrób jeden krok do tyłu – pierwszy i zarazem ostatni. Jestem pewien, że spodoba ci się ta podróż.