poniedziałek, 22 marca 2010

W TDD nie chodzi o testy

W trakcie przygotowań do szkolenia z TDD:
  K: Trzeba jakoś przekazać uczestnikom szkolenia, że w TDD nie chodzi o testowanie
  M: Jak to nie chodzi o testowanie? To o co chodzi?
  K: No chodzi o to, że testy są przykładami. Specyfikują to co właściwy kod ma robić.
  M: No dobra, ale na koniec to i tak jest testowanie. 
  P: No tak, ale możliwość odpalenia testów i zweryfikowania poprawności kodu jest tylko efektem ubocznym. Podstawowym celem jest dobre zrozumienie problemu oraz utworzenie dobrego designu.
  M: No tak, ale i tak testujemy a więc chodzi o testowanie
  P: Racja, ale nawet gdybyśmy mieli po zakończeniu implementacji stracić związane z nią testy, to i tak byśmy chcieli je pisać. Po prostu wartość z dobrego zrozumienia implementowanego zagadnienia i poprawności designu jest już wystarczająca. Możliwość późniejszego przetestowania - safety net - jest tylko dodatkiem do podstawowej wartości.

Ta trwająca z 15min dyskusja dała mi chwilę do zastanowienia się o co tak naprawdę chodzi w TDD. I choć M ma rację, że piszemy testy - więc testujemy, to nazwy test oraz TDD są zdecydowanie mylące. Już napiszę dlaczego.
Tworzenie oprogramowania to bardzo skomplikowane zadanie. Wielu z nas doświadcza tego przy prawie każdym wdrożeniu. Albo (mam nadzieję) nawet wcześniej - jak tester dobiera się do aplikacji i znajduje w niej masę błędów. Skąd one się biorą? Oczywiście czasem jest to jakieś drobne lenistwo programistów, ale większość przypadków to niekompletne rozwiązanie jakiegoś problemu lub niezauważenie jakiejś możliwości przepływu w aplikacji. Czyli żeby takich błędów nie było, trzeba by zapewnić dwie rzeczy:
1) dobre i kompletne zrozumienie rozwiązywanego problemu przez programistę
2) zapewnienie poprawnego wykonania aplikacji dla dowolnych parametrów (dowolnego przepływu kontroli w kodzie)

TDD jest odpowiedzią na oba te problemy. Jeśli zaczynamy pisanie kodu od jego wyspecyfikowania, czyli podania przykładów jego użycia, nasze zrozumienie zarówno rozwiązywanego problemu jak i samego pisanego kodu jest dużo większe. Po prostu spędzamy więcej czasu na zastanawianiu się nad problemem oraz projektowaniem rozwiązania. Ze względu na to, że w TDD definiujemy przykłady wykorzystania implementowanego kodu, bardzo często TDD nazywa się kodowaniem przez przykłady (coding by example). Na niższym poziomie piszemy po prostu przykłady zachowań obiektów (czyli jak chcemy wykorzystywać kod obiektu z innego obiektu). 
Powiedzmy więc, że chcemy napisać kod do obsługi pianina. Zaczynamy więc od reakcji na wciskanie klawiszy:

@Test
public void shouldPlaySoundWhenKeyPressed() {
// given
Piano piano = new Piano();

//when
piano.pressKey(Piano.Key.C);

//then
assertEquals(Piano.Key.C, piano.lastPlayedSound());
}
Na razie nic specjalnego. Teraz dodajemy pedał:

@Test
public void shouldPlayLongWhenSustainPedalPressed()  {
// given
Piano piano = new Piano();

//when
piano.pressSustain();
piano.pressKey(Piano.Key.C);
waitFor(5).seconds();
piano.releaseSustain();

//then
assertEquals(5, piano.lastSound().lasted(seconds));
}
I już widzimy, że klasa piano wymaga refaktoryzacji, więc poprzedni test powinien mieć w assert


 piano.lastSound().value()



W ten sposób kształtujemy API tak jak najwygodniej bę
dzie nam go używać, wymuszając na implementacji właściwy design i poprawiając czytelność kodu (czy waitFor(5).seconds() nie jest czytelniejsze od try { Thread.sleep(5000); } catch (InterruptedException e) {}?). Poprawiamy też nasze zrozumienie problemu - zorientowaliśmy się, że dźwięk ma nie tylko wysokość, ale również długość trwania, więc odpowiednio zmieniliśmy design.


To, że prawdziwe TDD pozwala na zapewnienie poprawnego wykonania kodu dla dowolnych parametrów jest jasne... jak się wie co to jest prawdziwe TDD ;-) Tak jak definiował je Kent Beck i jak zwykło się go nauczać, krok implementowania testu realizuje się przez napisanie najprostszego kodu, który powoduje, że test przechodzi. Jak najbardziej "po bożemu" jest więc pisanie trywialnego kodu po to tylko, żeby na drodze małych kroczków wymusić w kodzie istnienie wyłącznie tego, co jest wymagane i przetestowane. Tak więc dla powyższego przykładu, pierwsza implementacja metody pressKey wyglądałaby tak:


public Key lastPlayedSound() {
return Key.C;
}


Do przejścia testu wystarczy, a powoduje, że na pewno nie napiszemy żadnego kodu niepokrytego testami. Właściwa, kompletna implementacja powstanie już pewnie w ciągu następnych minut. Po napisaniu podobnego testu z inną nutą ten test już nie przejdzie i będzie trzeba napisać pełniejsze rozwiązanie. Choć na pierwszy rzut oka takie podejście wydaje się naiwne i bezsensowne, pomaga ono spojrzeć na problem z punktu widzenia zasady YAGNI - implementujemy wyłącznie to, czego wymaga od nas specyfikacja - czyli testy. W ten sposób łatwiej nam wpaść na prostsze rozwiązania.

TDD to więc prowadzenie naszego rozumowania i rozumienia problemu. To tworzenie sobie w głowie modelu implementowanej dziedziny. To również sposób na dbanie o jakość kodu z punktu widzenia design'u (czytelność, bo implementujemy tak jak chcemy wykorzystywać i na bieżąco refaktoryzujemy by nam jak najbardziej pasowało) oraz poprawności (prowadzenie przykładami wszystkich możliwych ścieżek wykonania kodu).
To, że za jakiś czas będziemy mogli spokojnie zmienić dowolną część tego kodu, pewni, że jeśli coś zepsujemy, to odpowiedni test nas o tym poinformuje, jest tylko efektem ubocznym. Miłym dodatkiem do poprawnego kodu. A mówili, że nie istnieje coś takiego jak darmowy lunch...

2 komentarze:

  1. Im dłużej tworzę soft, tym bardziej zgadzam się z poniższym stwierdzeniem:

    "The actual software is just a byproduct of the process of building an understanding of a given domain."
    Jonas Bandi [ http://blog.jonasbandi.net/2009/02/agile-lean-wait-moment.htm ]

    Gorąco polecam artykuł Naura "Programming as theory building", po przeczytanie którego autor stał się dla mnie ideowym mentorem.

    OdpowiedzUsuń
  2. To też jest podstawowe założenie DDD. Modelujemy, żeby zrozumieć to co modelujemy. Mój post miał trochę pokazać, że TDD to po prostu sposób na modelowanie tak, żeby jak najlepiej zrozumieć i zrealizować dziedzinę.

    OdpowiedzUsuń