Sobota, 08 grudnia 2007

Test-Driven Development w Agile, cz.1 (relacja z wykładu Sabre)

Wtorkowy wieczór, wpadam do gmachu AGH na kilka minut przed rozpoczęciem kolejnego wykładu firmy Sabre na temat lekkich metodologii wytwarzania oprogramowania. Przed salą niespodzianka - spotykam znajomego z czasów BEST-owych, którego nie widziałam już ho-ho. Za chwilę dołącza do nas drugi Beściak, też niespodziewanie :) W wesołych nastrojach, uzupełniając informacje o sobie nawzajem pytaniami w stylu "A co się teraz u ciebie dzieje??", wkraczamy na salę, gdzie Jakub Dziwisz ma opowiadać o podejściu test-driven development w Agile.

Początek wykładu opóźnił się wyjątkowo o kilka minut z powodów sprzętowych (przecież każdemu może się zdarzyć zapomnieć zasilacza, prawda? :)) i skończyło się na tym, że Kuba prowadził wykład na pożyczonym laptopie. Przechodząc do samego TDD, pojęcie to oznacza, że przed właściwym kodowaniem przygotowujemy najpierw testy (test-driven). Dla wielu osób takie podejście może wydawać się mało intuicyjne, lecz to podobno na początku normalne :-) Pozostaje kwestia wprawy, a gdy opanuje się dobrze tę sztukę, docenia się wszystkie zalety tego podejścia.

Okazało się, że w wielkim skrócie TDD można uprościć do trzech kroków: test - code - refactor.

TDD_t-c-r_time

Dobór kolorów na schemacie jest nieprzypadkowy. Jeśli kod programu, który właśnie sprawdzamy, nie przechodzi naszych testów, to pasek testowy będzie czerwony. Agile nakazuje nam, aby test był niewielki, odnoszący się do równie niewielkiej części kodu. W tej fazie powinniśmy się skupić na tym, aby przebarwić pasek na zielono. Gdy tak się stanie, będzie to oznaczać, iż testy jednostkowe przechodzą, a my możemy przejść do fazy refactoringu. Co istotne, refaktoryzujemy także testy, nie tylko właściwy kod aplikacji. Jeśli nie ma jeszcze takiej potrzeby, nie musimy oczywiście refaktoryzować. Cały cykl test-code-refactor powinien zająć około (ale nie mniej niż) 10 minut.

Ciekawostka

Na blogu Google poświęconym testowaniu widnieje hasło, które świetnie pasuje do podejścia TDD. Całość okraszona wesołym rysunkiem w słynnych (patrz wyżej) barwach.

If it ain't broke, you're not trying hard enough.
Google_testing

W TDD każda linijka kodu musi być wymuszona przez niedziałający test. Ponadto, podejście to wymusza wiele praktyk z innych dziedzin, jak simple design, YAGNI czy właśnie refaktoring. Na szczególną uwagę zasługuje zasada You Ain't Gonna Need It, mająca swoje uzasadnienie w długości cyklu test-code-refactor. Zgodnie z tą zasadą nie powinniśmy dodawać nowych funkcjonalności/elementów kodu dopóki nie są one konieczne na danym etapie. W przeciwnym razie prowadzić to może do opóźnień w pracy (nowe linijki trzeba obkomentować, przetestować, uwzględnić w dokumentacji, etc) i duża szansa, że nie zmieścilibyśmy się w krótkim cyklu. Takie "wybieganie myślą naprzód" może też sprawić, że nasz kod zacznie wyglądać chaotycznie, gdyż pewne jego elementy mogą okazać się nieprzemyślane. Wprowadzić możemy też nieświadomie pewne ograniczenia, które staną na przeszkodzie, gdy będziemy chcieli zaimplementować te rzeczy, które będą akurat faktycznie niezbędne. No i zawsze jeszcze pozostaje efekt lawiny, gdzie zaczynamy od "bo ja chciałem tylko dwie linijki dopisać", a kończymy na wielkim kawałku kodu.

TDD -- jak to się robi?

Przede wszystkim, testy powinny być zautomatyzowane. Nie należy także zapominać, iż stają się one tym samym integralną częścią systemu. Nie ma już sytuacji, w której liczy się tylko kod aplikacji, a testy traktowane są jako dodatek.

  1. Lista ToDo - czyli jakie funkcjonalności chcemy testować.
  2. Test - zgodnie z powyższym, test powinien być niewielki. Zaczynając, z listy ToDo bierzemy najprostsze testy. Testy sprawdzają bardziej klasę niż metodę.
  3. Kod - uwaga jak wyżej, gdyż mamy na uwadze 10-minutowe cykle.
  4. Refaktoring - gdy uznamy za słuszne, poprawiamy kody i testy.

Rodzaje testów jednostkowych

  • tzw. startery -- najprostsze testy, które piszemy na początek.
  • regresyjne -- testy robiące za "przestrogę". Są to testy, które nie przechodzą przy konkretnym błędzie. Mają na celu zapewnienie, że po naprawieniu błędu, ponownie się on nie pojawi, gdyż będziemy mieli test, który jest przygotowany na takie sytuacje.
  • uczące -- jeśli chcemy się szybko nauczyć jakiegoś API
  • wyjaśniające -- podobne do testów uczących. Jeśli nie mamy czasu, aby zgłębiać jakiś fragment kodu, możemy napisać test, który nam to w szybki sposób wyjaśni.

Wzorce TDD

Nie ma narzuconego porządku testów. Możemy je pisać w dowolnej kolejności, gdyż testy nie zależą jedne od drugich. Jeśli test jest wyjątkowo duży, powinniśmy podzielić go na mniejsze części, z których każda stanie się osobnym testem. Ponadto, minimalizować należy odwołania do dysku twardego.

Istnieje także kwestia mapowania - czy testy jednostkowe mogą współdziałać z innymi klasami niż te, które testujemy? Sprawa ta dotyczy klas, które są w jakiś sposób ze sobą połączone.

Pojawiają się tu dwa pojęcia, związane nie tylko z TDD - stub i mock. Oba dotyczą testowania, jednak ich charakter i przeznaczenie jest inne.
Stub odzwierciedla podejście do testowania state-based, czyli oparte o stan. Stuby zawsze ustawiamy recznie. W praktyce wygląda to w ten sposób, iż mając funkcję bez kodu, podstawiamy jakąś gotową wartość zwracaną. W ten sposób nie koncentrujemy się na tym, jak ta funkcja działa, lecz na aspekcie jej użycia w programie.
Mocki reprezentują podejście interaction-based i w przeciwieństwie do stubów potrafią weryfikować swoje zachowanie. Ponieważ ja w mojej praktyce programistycznej spotkałam się tylko ze stubami, po szczegółowy opis mocków odsyłam do fachowej literatury :)

Co testować?

Wszystko, z czym wiąże się jakiekolwiek ryzyko w działaniu. Wszystko, co podejrzewamy o jakieś błędy. Wszystko, co intuicja podpowiada.

Czego NIE testować?

  1. Funkcji typu get/set.
  2. Metod toString (Java), chyba że cały fragment jest newralgiczny/"podejrzany".
  3. Standardowego API (wszelkie odniesienia były do Javy, jednak uwagę tą można także zastosować do C++).
  4. Kodu innych programistów, stawiając na zaufanie. Wyjątkiem mogą być sytuacje, kiedy mamy uzasadnione obawy/podejrzenia.
  5. Klas wprowadzonych w refaktoringu.
  6. Metod prywatnych, gdyż są one testowane przy okazji metod publicznych.

Code coverage

Code coverage, czyli pokrycie kodu jest miarą stosowaną przy testowaniu. Określa stopień, w jakim kod programu został przetestowany, zaś bazując na jego dostępności, code coverage wchodzi w skład testów tzw. białej skrzynki (white box testing).

Code coverage:
- what's not tested
- not what's tested

Trudności przy TDD

Stosując podejście test-driven development, przy pewnego rodzaju aplikacjach bądź systemach możemy napotkać trudności. Warto mieć ich świadomość, aby wiedzieć, czego unikać i jakich zasad najlepiej się trzymać.

  • Obsługa baz danych, choć tu sytuacja się podobno poprawiła jeśli chodzi o Javę dzięki bibliotekom Hibernate i Spring.
  • Wielowątkowość, przy której testy jednostkowe muszą być przeprowadzane dla każdego wątku z osobna. Niemożliwe jest, aby jeden unit test obejmował równocześnie wszystkie wątki.
  • GUI, które najlepiej utrzymywać jak najprostsze.

Złe testy bolą...

Po czym poznać, że testy zostały źle napisane? Przede wszystkim, długo się uruchamiają. Jeszcze gorzej, jeśli długie uruchamianie poprzedzone jest długim setup-em. Kolejnym wyznacznikiem jest "psucie się" testów z niewiadomych przyczyn. Uwagę należy także zwrócić na dane testowe, które mogą być źle/pobieżnie dobrane.

. . .

Wykład był dość mocno praktyczny, tzn. bawiliśmy się w Eclipsie w grupowe extreme programming. Mnie osobiście utwierdziło to tylko w przekonaniu, że takie pisanie na bieżąco "z całą klasą" jest na dłuższą metę nużące... zwłaszcza, gdy ktoś używa na codzień języka innego niż Java. I niby większości można się domyślić, bo na tym poziomie bazowaliśmy na dość podstawowych poleceniach, to jednak nie lubię się zastanawiać, co robi jakaś funkcja, gdy w tym czasie powinnam śledzić, co robi prowadzący. Wielkim minusem jest też to, że po powrocie do domu nie mogłam sobie ot tak po prostu otworzyć u siebie Eclipsa i przećwiczyć tego, co robiliśmy na zajęciach. Może czas się przekwalifikować?


Przeczytaj także wcześniejsze relacje:

  1. First things first, czyli nowoczesne podejście do wytwarzania oprogramowania
  2. Jak planować projekty Agile?
  3. Estymowanie i planowanie historyjek użytkownika w Agile, część 1
  4. Planowanie user stories w Agile, część 2

Ciekawe strony o TDD:

Poprzedni wpis: « Chodź, pomaluj mi majtki ;-)


Komentarze:
  1.   Stanisław 'dozzie' Klekot (Sobota, 08 grudnia 2007, 03:21:30):

    Czyli zamiast projektu długoplanowego ("czego nasz klient oczekuje od programu?") stawiamy na krótkoterminowe "napraw co zepsułeś". Bardzo fajna metodologia.

  2.   sisi (Sobota, 08 grudnia 2007, 09:40:18):

    "Dla wielu osób takie podejście może wydawać się mało intuicyjne"

    @Kasiaro - powinno być "Dla wielu developerów..." ;-) Prawda jest taka, że np. w dość popularnej metodologii V-Model wszystkie testy: akceptacyjne, systemowe, integracyjne i jednostkowe powstają PRZED samym kodowaniem. I fakt faktem, że developerzy w ogóle strasznie się krzywią, jak mają jakieś przeprowadzone przez siebie testy udokumentować.

    @dozzie - to zupełnie nie tak. Punktem wyjścia każdego projektu były, są i będą wymagania użytkownika, przypadki użycia itp. TDD to tylko jedna z metod developmentu w ramach szeroko pojętej metodologii Agile/XP. W TDD nie chodzi o trworzenie kodu na "hura". Tylko o tworzenie takich testów, które na końcu będą testować właśnie oczekiwaną przez użytkownika funkcjonalność. Potem pisze się kod tak, aby takie testy ostatecznie przeszedł - no i oczywiście iteracje to podstawa. A i jeszcze jedno - w TDD testy są jednocześnie dokumentacją stworzonej aplikacji (sic!) - a o już daje do myślenia, jak powinny one wyglądać.

  3.   Kasia Rogowska (Sobota, 08 grudnia 2007, 10:56:46):

    sisi: Developerzy byli w domyśle :)

    dozzie: Jak napisała sisi, nie chodzi o naprawianie. Jak najbardziej interesuje nas, czego klient oczekuje (w końcu bez tego ani rusz...), jednak odbywa się to w trochę inny sposób.

  4.   MiB (Sobota, 08 grudnia 2007, 11:25:17):

    'Mnie osobiście utwierdziło to tylko w przekonaniu, że takie pisanie na bieżąco "z całą klasą" jest na dłuższą metę nużące...'

    Zgadzam się. Mnie w ogóle denerwuje kiedy ktoś siada przed laptopem i zaczyna kodzić - pisze kilka linijek i później je omawia.
    Zazwyczaj jest wtedy tak, że dokładnie omawiane są trywialne linijki, a te co ciekawsze skrzętnie się omija (co pokazuje stan wiedzy prelegenta :P).
    Poza tym - takie kodowanie podczas prezentacji zazwyczaj jest oznaką nieprzygotowania/potraktowania tematu "po łebkach".
    Akurat w przykładach przedstawionych przez Kubę się orientowałem (choć "moim językiem" jest PHP, a ostatnio C#), ale dla mnie ważniejsze było to co pokazywały późniejsze slajdy, a co zostało omówiona pobieżnie, np.: bardzo interesuje mnie wykorzystanie TDD w aplikacjach webowych, co nie było poruszone (nie, nie mogłem zapytać na końcu, bo Kuba chciał już z Wami [laborantami] rozmawiać, a ja szedłem na wykład o ASP.NET 2 piętra niżej ;) ).

    Pozdrawiam

    p.s. prawie Ci się z tymi Mockami i Stubami udało ;)

  5.   Kasia Rogowska (Sobota, 08 grudnia 2007, 11:39:12):

    MiB: Zawsze możesz wysłać Kubie pytanie na maila. Zdaje się, że po pierwszym wykładzie zaproponował taką formę "konsultacji".

    p.s. No właśnie gdyby nie te mocki, to bym pewnie coś wygrała ;-) (Pytanie co, ale przyznam, że akurat to mi umknęło, że za to może być "nagroda", więc się zdziwiłam jak Kuba o tym powiedział po mojej wypowiedzi :D) Ej, i ja nadal nie wiem, który to Ty byłeś ;)

  6.   MiB (Sobota, 08 grudnia 2007, 11:44:10):

    @Kasia: tak, Kuba mówił coś o nagrodzie, tylko nie wiedział co to będzie :P
    A widzisz - gdybyś się przypadkiem obróciła do tyłu, to wiedziałabyś który to ja :P
    (miałem podejść po wykładzie, ale - jak napisałem w poprzednim komentarzu - ja szedłem na kolejny wykład, a Ty zostałaś).

  7.   Seban (Niedziela, 09 grudnia 2007, 12:21:43):

    A aplikacjach webowych TDD sprawdza się dobrze. Oczywiście wszystko zależy od tego jakie testy się pisze. W Ruby on Rails nie ma problemów z działaniem zgodnie z TDD czy BDD.

DODAJ KOMENTARZ: