Artykuły | 30 lipiec, 2019

Test-Driven Development (TDD) na co dzień

Z biznesowego punktu widzenia tym, czego oczekuje klient, jest działająca aplikacja. Nic więc dziwnego, że dla niego testy jednostkowe są zwykle jedynie dodatkiem, z którego najchętniej by zrezygnował. I rzeczywiście – do niedawna nie przykładano tak wielkiej uwagi do pisania testów jednostkowych i w takiej konwencji powstało co najmniej kilka poważnych aplikacji, o których wiem, a wśród nich programy do gospodarowania magazynami, wspierające działanie sklepów, instytucji finansowych i banków. Jednak na przestrzeni ostatnich 15 lat obserwujemy na szczęście ewolucję sposobu postrzegania testów, a dawne podejście staje się powoli „prehistorią developmentu”. Przenieśmy się więc do współczesności i przyjrzyjmy stosowaniu Test-Driven Development, czyli podejściu, które jest ‘state of the art’ współczesnych testów oprogramowania.

Z tego artykułu dowiesz się:

  • Czym jest Test-Driven Development,
  • Czym może skutkować brak TDD – case study,
  • Jaki jest schemat ideowy Test-Driven Development,
  • Jak minimalizować liczbę testów,
  • Jak zadbać o czytelność testów jednostkowych,
  • Jakie są korzyści z TDD dla klienta i developera.
Test-Driven Development (TDD) na co dzień

Test-Driven Development

TDD to jedna z technik zwinnego wytwarzania oprogramowania. Technika Test-Driven Development, której autorem jest Kent Beck, jeden z autorów Agile Manifesto, polega na wielokrotnym powtarzaniu cyklu Red – Green – Refactor, o czym piszę poniżej. Tym, co wyróżnia technikę Test-Driven Development, jest to, że programista rozpoczyna pracę od pisania testów jeszcze nienapisanej funkcji. Pozwala także na dodawanie jakiejkolwiek nowej funkcjonalności do rozwijanej aplikacji, minimalizując ryzyko pojawienia się przypadkowych błędów w niezmodyfikowanych obszarach programu, o których powiązaniach po kilku miesiącach mogliśmy zapomnieć. Jedną z zalet Test-Driven Development jest to, że modyfikując lub dodając funkcjonalność, automatycznie otrzymujemy raporty o wszystkich powiązanych obszarach, popsutych przez nasze zmiany. Daje to szansę, by szybko je poprawić i nie dopuścić do ich wdrożenia wraz z funkcjonalnością. Podejście Test-Driven Development pozwala projektować zaawansowane wzorce i dzięki automatyzacji sprawia, że design aplikacji lub oprogramowania jest czytelny i przejrzysty. Sczególnie dobrze sprawdzają się w przypadku rozbudowanych projektów. O Test-Driven Development można by pisać dużo – w tym artykule chciałbym skupić się na stosowaniu TDD w praktyce oraz korzyściach płynących z tego podejścia.

Przeczytaj także:

Brak stosowania TDD – case study

Przed laty, gdy stawiałem pierwsze kroki jako programista, trafiłem do projektu, w którym wspólnie z kolegą po fachu mieliśmy za zadanie stworzyć dedykowany konfigurator dla producenta okien. Sam fakt złożoności technologicznej produkcji okien powinien zdeterminować maksymalną przezorność. Użytkownik aplikacji mógł skonfigurować okno, bazując na parametrach zarówno standardowych, jak i niestandardowych. W zależności od wymiarów pewne opcje były dostępne lub nie. Użytkownik miał do pokonania kilka-kilkanaście kroków z wieloma możliwościami dla każdego. Na ostateczną cenę wpływały różne opcje i czynniki.

W projekcie tym zaobserwowałem co najmniej kilka nie najlepszych praktyk, np. brak kodu testującego czy code review. Manager i klient nie wiedzieli jednak, że w miarę możliwości staraliśmy się recenzować pracę, którą dostarczaliśmy, a której efekty były przeznaczone do stosowania bezpośrednio na środowisku produkcyjnym.

Przez cały czas pracy przy projekcie czułem, że działam pod ogromną presją. Nie polemizowałem, gdy powiedziano mi, że w języku PHP (w którym pisaliśmy), nie przeprowadza się testów jednostkowych. W efekcie kod, który po modyfikacjach rozbudowywaliśmy, wymagał wielokrotnych poprawek. Gdybyśmy wówczas stosowali Test-Driven Development, udałoby się uniknąć wielu frustracji związanych z niekończącymi się poprawkami. Jak więc działają TDD?

Schemat ideowy Test-Driven Development

Schemat ideowy Test-Driven Development (Red – Green – Refactor) wydaje się wskazywać na coś bardzo prostego. Przyjrzyjmy się jego poszczególnym elementom.

TDD cykl Red Green Refactor

Red – to nic innego jak etap pisania testów jednostkowych dla funkcjonalności, które uruchomione na aplikacji bez zaimplementowanego rozwiązania, siłą rzeczy wygenerują raport o błędach, wskazujący w najlepszym razie na niepoprawne działanie aplikacji w tym obszarze (czyli jej brak). W większości współczesnych narzędzi programistycznych do pisania kodu raport taki będzie zabarwiony charakterystycznym kolorem czerwonym. Ten etap jest bardzo istotny. Właśnie teraz developer dokonuje rozpoznania problemu, nad którego rozwiązaniem pracuje, analizując dotychczasowy kod opisujący ten obszar (jeżeli tylko taki istnieje). To również na tym etapie spoczywać będzie na jego barkach największy ciężar podczas implementacji nowej funkcjonalności.

Green – to kolejny etap, w którym czerwień z kolejnych raportów po uruchamianiu testów jednostkowych będzie w sposób progresywny przechodzić do barwy zielonej. Przejście testów będzie sygnalizowane przez IDE (Integrated Development Environment). To na tym etapie dokonywać się będzie właściwa implementacja nowej funkcjonalności.

Refactor – na tym etapie wprowadzamy poprawki do nowo zaimplementowanych funkcjonalności. Możliwie uwspólniamy zastosowane również w innych miejscach podobne rozwiązania. Dla właściwej implementacji poprawiamy czystość napisanego kodu i przygotowanych przypadków testowych. Ta faza nie oznacza końca testów, gdyż po kolejnych modyfikacjach kodu cykl Red – Green – Refactor powtarza się.

Test-Driven Development – czy można zacząć od fazy Green?

Możemy oczywiście osiągnąć podobne rezultaty, zaczynając pracę od fazy Green, poprzez Red, aż do Refactoringu. Jako że jest całkiem spora szansa na zaimplementowanie aplikacji, zanim jeszcze przygotujemy do niej zestawy testów jednostkowych, taka ścieżka wydaje się nawet kusząca. Jest jednak haczyk w takim podejściu. W ferworze walki z kolejnymi funkcjonalnościami możemy zacząć przywiązywać coraz mniejszą uwagę do jakości naszych scenariuszy testowych. Nie twierdzę co prawda, że tak musi być, ale prędzej czy później nasz kod testujący może nie pokrywać możliwych scenariuszy lub nawet stać się bezwartościowy. Rośnie wówczas ryzyko wystąpienia poważnych błędów na środowisku produkcyjnym – błędów trudnych do naprawy i najczęściej w rzadko używanych obszarach. To często skutkuje sporymi wydatkami po stronie klienta związanymi z naprawą błędów.

TDD test – zalety rozpoczynania od fazy Red

Dużo się mówi o tym, że rozpoczynając cykl realizacji funkcjonalności od przygotowania testów jednostkowych, będziemy potrzebować więcej czasu na zadanie niż w sytuacji, gdy prace zaczniemy od implementacji. To może być prawdą, gdy programista zaczyna pracę nad nowym projektem i wszystko musi przygotować od zera. Wówczas może pojawić się potrzeba utworzenia dodatkowych klas wspierających tworzenie przypadków testowych lub wdrożenia jednego z dostępnych template engine’ów do generowania requestów albo eventów. W nieco późniejszych okresach rozwoju aplikacji, jeżeli pojawią się jakieś różnice, to raczej na marginalnie niskim poziomie.

Jak minimalizować liczbę testów?

Czy nie powinniśmy tworzyć scenariuszy testowych dla każdej, nawet najmniejszej drobiny naszego kodu? Wiadomo, że najmniejsze nawet drobiny są składowymi pewnych większych struktur i zwykle jest szansa na to, by można je wspólnie opisać przy pomocy kilku rozsądnych scenariuszy przypadków brzegowych. Oczywiście zdarzają się takie obszary, jak na przykład walidatory czy kalkulatory, których często nie sposób zbadać i pokryć niewielką ilością przekrojowych testów. Chciałbym jednak podpowiedzieć, jak minimalizować liczbę testów, oszczędzając tym samym czas i wykorzystując na co dzień pełen potencjał TDD.

Testy a nieistniejące przypadki biznesowe

Ciekawym zagadnieniem jest pokrywanie testami nieistniejących przypadków biznesowych. Dylematu tego nie napotkamy zwykle podczas tworzenia testów dla walidatorów czy filtrów, ale podczas przypadku takiego jak pisanie testów z działania tworzonej funkcjonalności – już tak. Chodzi zwykle o sytuacje, które z takich czy innych powodów nie zadzieją się nigdy – jak wtedy, gdy nasza funkcjonalność zależeć będzie od danych ze ściśle określonych i zdefiniowanych zbiorów. Przykładem może być wybór kodu kraju wystąpienia szkody komunikacyjnej, gdy wykorzystujemy przeznaczoną funkcjonalność do ich rejestracji. Mając świadomość, że użytkownik będzie mógł wskazać jeden ze zdefiniowanych w bazie kodów, wiemy, że nie będzie miał on opcji wyboru innego lub nieistniejącego kodu podczas rzeczywistej pracy aplikacji. Czy w takim razie ma sens testowanie tego, jak zareaguje aplikacja, gdy użytkownik wybierze kod kraju „XXL”, jeżeli do dyspozycji zawsze będzie miał np.: tylko „PL” i „DE”?

Stosuj parametryzację

Kolejnym przykładem minimalizacji liczby testów z zachowaniem pokrycia jest parametryzacja, czyli technika konsolidowania scenariuszy, do której używania chciałbym zachęcić. Mimo że w tym podejściu nadal tworzymy tyle samo przypadków testowych, to jednak samego kodu testowego jest mniej. Z racji wielości warunków koniecznych do zbadania takie rozwiązanie sprawdzić się może podczas testowania klas walidatorów, kalkulatorów itp. Jego wadą może być spadek czytelności opisów takich testów zawartych w nazwach metod, co zwykle towarzyszyć może wszelkim aktom konsolidacyjnym. Nie będzie to jednak żadną przeszkodą ani problemem, jeśli w naszym projekcie zdecydujemy się na wykorzystanie współczesnych narzędzi, jak Spock Framework, który dostarczy nam znacznych ułatwień podczas samego pisania asercji. Ułatwienia te otrzymamy stricte z języka Groovy, na którym bazuje wspomniany framework. Język Groovy zapewnia większą przejrzystość w kontekście testów i dokumentacji. W łatwy sposób uzyskamy nazwy scenariuszy, ale też możliwość dodania dodatkowych opisów. Pozwoli to także zachować czytelną formę przypadków testowych, zgodną z „given, when, then”. Na pewno pozwoli to lepiej udokumentować nasze funkcjonalności.

Możemy niekiedy trafić na zwolenników tworzenia osobnych przypadków per scenariusz, czyli niekonsolidowania. Wówczas jednak wracamy do tematu przyszłego utrzymania wszystkich testów, gdy nasza aplikacja będzie się rozwijać. Zawsze jest coś za coś… Zwykle najrozsądniej jest jednak zachować umiar. Gdy w czasie poszukiwania złotego środka pojawi się temat rozbudowany i skomplikowany, to pomimo wielu podobieństw w przypadkach testowych moim zdaniem warto rozpisać je z osobna, jeden po drugim. Kategoryczność podejścia może doprowadzić do zagubienia użyteczności, jaką powinny wnieść testy jednostkowe (unit tests) do naszej codziennej pracy. Skupiając się zbytnio na formie, możemy zatracić rozumienie funkcji scenariuszy testowych.

Zadbaj o czytelność testów jednostkowych

Tak samo ważne jest, by skupiając się na funkcji samego przetestowania analizowanych obszarów, nie zatracić formy. Estetyka i opisowość w nazwach scenariuszy, użytych nazwach metod czy zmiennych dokumentuje nam obszary funkcjonalności i pozwala wykorzystywać test w sposób świadomy i zrozumiały. Zaryzykuję stwierdzeniem, że miarą użyteczności testu jednostkowego będzie zarówno rozsądne pokrycie testem badanego obszaru, jak i czytelność jego implementacji. Takie podejście uchroni nas przed sytuacją, której wielu mogło doświadczyć, a mianowicie kiedy po dłuższym czasie trudno jest zacząć pracę nad rozwojem dawno zaimplementowanego obszaru. Bez starannie napisanych, czytelnych testów nasza praca będzie prawdziwą drogą przez mękę.

Korzyści z TDD dla klienta i developera

TDD dla developera to przede wszystkim:

  • wyższy komfort pracy – również pracy z istniejącym kodem dzięki mniejszej liczbie błędów,
  • dokumentacja funkcjonalności na wysokim poziomie,
  • lepsza wiedza biznesowa, a tym samym lepsza znajomość produktu,
  • większa szansa na czysty kod,
  • łatwiejszy start pracy w projekcie
  • redukcja stresu i mniej presji w pracy,

TDD dla klienta to:

  • wyższy poziom pewności co do poprawności działania aplikacji i mniej błędów,
  • możliwość przewidywania kosztów utrzymania aplikacji oraz wprowadzania modyfikacji do już istniejących rozwiązań,
  • oszczędności na wdrażaniu nowych programistów do pracy przy projekcie,
  • szybsze lokalizowanie i naprawianie zaistniałych błędów,
  • większa szansa na interesujące sugestie od developera co do wprowadzanych rozwiązań, dzięki lepszej znajomości obecnych funkcjonalności,
  • TDD oznacza lepszy kod, a więc i lepszą aplikację.

Podsumowanie

Mam nadzieję, że udało mi się przekazać, co wnosi TDD do codziennej pracy i jakie mogą być konsekwencje niestosowania go w praktyce. Wspomniane przeze mnie zasady sprzyjające minimalizacji testów: parametryzacja, minimalizacja ilości, pomijanie nieistniejących przypadków biznesowych w scenariuszach oraz czytelność kodu testowego nie wynikają bezpośrednio z definicji TDD. Nie sposób jednak w praktyce osiągnąć prawdziwego rozwoju za pomocą testów bez rozsądnego ich przestrzegania. Wierzę, że udało mi się zaprezentować, czym jest metodyka Test-Driven Development i jak wielką wartość niesie ze sobą bazująca na niej produkcja dedykowanego oprogramowania.

Przeczytaj także: Clean architecture

Od ponad siedmiu lat java developer. Prywatnie miłośnik jazzu i piwa rzemieślniczego. Uważa, że w pracy developera najważniejsze są trzy rzeczy: dbałość o czysty kod, solidne testy jednostkowe i konsensus.

Zapisz się do newslettera, ekskluzywna zawartość czeka

Bądź na bieżąco z najnowszymi artykułami i wydarzeniami IT

Informacje dotyczące przetwarzania danych osobowych

Zapisz się do newslettera, ekskluzywna zawartość czeka

Bądź na bieżąco z najnowszymi artykułami i wydarzeniami IT

Informacje dotyczące przetwarzania danych osobowych

Zapisz się do newslettera, aby pobrać plik

Bądź na bieżąco z najnowszymi artykułami i wydarzeniami IT

Informacje dotyczące przetwarzania danych osobowych

Dziękujemy za zapis na newsletter — został ostatni krok do aktywacji

Potwierdź poprawność adresu e-mail klikając link wiadomości, która została do Ciebie wysłana w tej chwili.

 

Jeśli w czasie do 5 minut w Twojej skrzynce odbiorczej nie będzie wiadomości to sprawdź również folder *spam*.

Twój adres e-mail znajduje się już na liście odbiorców newslettera

Wystąpił nieoczekiwany błąd

Spróbuj ponownie za chwilę.

    Get notified about new articles

    Be a part of something more than just newsletter

    I hereby agree that Inetum Polska Sp. z o.o. shall process my personal data (hereinafter ‘personal data’), such as: my full name, e-mail address, telephone number and Skype ID/name for commercial purposes.

    I hereby agree that Inetum Polska Sp. z o.o. shall process my personal data (hereinafter ‘personal data’), such as: my full name, e-mail address and telephone number for marketing purposes.

    Read more

    Just one click away!

    We've sent you an email containing a confirmation link. Please open your inbox and finalize your subscription there to receive your e-book copy.

    Note: If you don't see that email in your inbox shortly, check your spam folder.