Test Driven Development

Czym jest Test Driven Development (TDD)?

TDD to metoda tworzenia oprogramowania, polegająca na równoczesnym pisaniu testów oraz kodu właściwego. Podejście to zakłada, że przed napisaniem właściwej funkcjonalności aplikacji, tworzymy test. Podczas tworzenia kodu można wyróżnić trzy fazy: Red, Green oraz Refactor, które następują po sobie. Jest to Mikrocykl, który powtarza się w kółko: Red-Green-Refactor.

TDD – czym jest faza Red?

Podczas tej fazy dodajemy tylko jeden test, który nie przechodzi. Ten nowy test sygnalizuje, że potrzebne jest utworzenie odpowiedniego kodu, który spowoduje, że test będzie przechodził.

TDD – czym jest faza Green?

W tej fazie dodajemy kod potrzebny do przejścia nowo utworzonego testu. Staramy się to robić najprościej, jak się da, aby jak najszybciej otrzymać pozytywny wynik testu. Ważne jest, aby przy tym inne testy, utworzone w wcześniejszych cyklach, także przechodziły. Po uzyskaniu wyników pozytywnych kończymy tę fazę i nie dodajemy już ani jednej linijki kodu.

TDD – czym jest faza Refactor?

W fazie Refactor poprawiamy napisany kod. Na przykład, poprawiamy nazwy zmiennych, eliminujemy redundancję. Robimy po prostu "kosmetykę", nie zmieniając funkcjonalności. Najlepiej jest po wykonaniu zmian kosmetycznych uruchomić testy, aby sprawdzić, czy niczego nie zepsuliśmy. Po tej fazie powtarzamy Mikrocykl: Red-Green-Refactor, aż do momentu, kiedy napiszemy wszystkie testy pokrywające logikę biznesową.

Czy TDD to jedyna słuszna metoda tworzenia kodu?

TDD jest rozbudowanym narzędziem, które w odpowiednich warunkach może ułatwić, uprościć i usprawnić proces rozwoju kodu. Jednak nie zawsze mamy do czynienia z takim zadaniem. Podejście TDD nie sprawdzi się w projekcie prostym, mało skomplikowanym – stosowanie wówczas TDD może znacznie wydłużyć czas realizacji tego projektu, zaś zalety jego stosowania mogą być niewielkie. Podejście klasyczne również będzie lepsze, gdy mamy do czynienia z problemem, którego rozwiązanie nie jest dla nas oczywiste. W takiej sytuacji najpierw staramy się jak najszybciej napisać kod, dzięki któremu będziemy mieli pewność, że potrafimy poprawnie rozwiązać ten problem.

TDD – jakie są zalety tej techniki?

Po pierwsze, Test Driven Development znacznie zmniejsza możliwość wystąpienia błędów w aplikacji. Unikamy błędów, ponieważ aplikacja jest przetestowana na bieżąco. Dzięki temu można znacznie ograniczyć czas, ponieważ nie trzeba go marnować na szukanie błędów. Kolejną zaletą jest łatwiejsza rozszerzalność aplikacji. Gdy wracamy po jakimś czasie do projektu, zapewne wiele zapomnimy. Jednak przeprowadzone testy stanowią dokumentację, dzięki której szybciej wchodzimy w projekt i jesteśmy w stanie go zaktualizować, nie narażając się na nowe błędy. TDD pomaga utrzymać stabilność kodu i ułatwia wprowadzanie zmian w późniejszych fazach projektu.

Implementacja przy użyciu TDD

Poniższa implementacja jest wykonana w języku Java. Do robienia asercji wykorzystuję bibliotekę org.hamcrest.

Moim zadaniem jest stworzenie prostej klasy reprezentującej samochód. Założenia, które ma spełniać stworzona klasa:

  1. Nowy obiekt ma mieć licznik kilometrów wyzerowany.
  2. Samochód ma zapisywać przejechane kilometry.
  3. Samochód ma mieć możliwość wyzerowania stanu licznika.

Zabieramy się za pisanie aplikacji, oczywiście zaczynamy od stworzenia testu, czyli od fazy Red:

@Test

 void nowyObiektKlasySamochodPowinienMiecPrzejechanychZeroKilometrow () {

        //given

        Samochod samochod = new Samochod();

        //when

        //then

        assertThat(samochod.pobierzPrzejechaneKilometry, equalTo(0));

}

Sprawdzamy, czy test przechodzi. Mamy błąd:

java: cannot find symbol
symbol: method pobierzPrzejechaneKilometry()
location: variable samochod of type com.test.testing.Samochod

Przechodzimy zatem do fazy green. Implementujemy poniższy kod:

public class Samochod {

    int przejechaneKilometry;

    public Samochod() {
        this.przejechaneKilometry = 0;
    }

    public int pobierzPrzejechaneKilometry() {
        return przejechaneKilometry;
    }
}

Sprawdzamy test – przechodzi. Przechodzimy do fazy Refactor. Patrzymy czy można w kodzie coś poprawić. Moim zdaniem, na razie nie ma takiej potrzeby, więc kończymy pierwszy Mikrocykl i przechodzimy do kolejnego, zaczynamy oczywiście od fazy Red. Piszemy kolejny test:

@Test
void obiektKlasySamochodPowinienZapisywacPrzejechaneKilometry () {
    //given
    Samochod samochod = new Samochod();
    //when
    samochod.podroz(15);
    //then
    assertThat(samochod.pobierzPrzejechaneKilometry(), equalTo(15));
}

Sprawdzamy czy na pewno test kończy się nie powodzeniem i przechodzimy do Fazy Green. Rozszerzamy naszą klasę Samochód, dokładając metodę „podróż”:

public void podroz(int iloscKilometrow) {
    przejechaneKilometry += iloscKilometrow;
}

Sprawdzamy obydwa napisane testy – pojawiają się zielone haczyki – a zatem test zakończony sukcesem – możemy zająć się ostatnią fazą tego cyklu: Refactor. Tutaj, tak jak poprzednio, nie ma potrzeby ingerowania. Drugi Mikrocykl zakończony, zaczynamy kolejny – tworzymy test:

@Test
void obiektKlasySamochodPoKliknieciuZerujLicznikPowinienGoWyzerowac (){
    //given
    Samochod samochod = new Samochod();

    samochod.podroz(15);
    //when
    samochod.zerujLicznik();
    //then
    assertThat(samochod.pobierzPrzejechaneKilometry(), equalTo(0));
}

java: cannot find symbol

  symbol:   method zerujLicznik()

  location: variable samochod of type com.test.testing.Samochod

Następnie uzupełniamy nasz kod właściwy:

public void zerujLicznik() {
    przejechaneKilometry = 0;
}

Po zaimplementowaniu powyższej metody wszystkie testy przechodzą. Możemy zabrać się za Refactor. Możemy przenieść tworzenie obiektu samochodu przed wszystkie testy, aby tego nie powtarzać. Wówczas klasa testowa wygląda następująco:

class SamochodTest {
    Samochod samochod = new Samochod();
    @Test
    void nowyObiektKlasySamochodPowinienMiecPrzejechanychZeroKilometrow () {
        //given
        //when
        //then
        assertThat(samochod.pobierzPrzejechaneKilometry(), equalTo(0));
    }

    @Test
    void obiektKlasySamochodPowinienZapisywacPrzejechaneKilometry () {
        //given
        //when
        samochod.podroz(15);
        //then
        assertThat(samochod.pobierzPrzejechaneKilometry(), equalTo(15));
    }

    @Test
    void obiektKlasySamochodPoKliknieciuZerujLicznikPowinienGoWyzerowac (){
        //given
        samochod.podroz(15);
        //when
        samochod.zerujLicznik();
        //then
        assertThat(samochod.pobierzPrzejechaneKilometry(), equalTo(0));
    }
}

Sprawdzamy jeszcze raz, czy testy przechodzą – testy zakończyły się pomyślnie, przechodzimy do kolejnego cyklu i tak dalej i tak dalej.

Myślę, że pokrótce przybliżyłem, jak wygląda tworzenie aplikacji przy metodyce TDD. Jak widać, można by to zrobić zapewne szybciej bez testowania, ale w momencie, gdy ta aplikacja się rozrośnie i zdarzy się, że coś nie będzie działać tak, jak powinno, to może być tak, że więcej czasu spędzisz na szukaniu błędów niż gdybyś go poświęcił na pisanie aplikacji w podejściu TDD. Dodatkowo, przynajmniej ja, mam małą satysfakcję za każdym razem, gdy wcześniej niedziałający test zapala się na zielono.

Podsumowanie

TDD nie jest złotym środkiem na wszystko, ale gdy mamy do tego odpowiednie warunki, to stosowanie podejścia TDD pozwoli nam zaoszczędzić czas oraz stworzyć aplikacje, której błędy będą znikome. Przed przystąpieniem do projektu warto się zastanowić nad sposobem pisania aplikacji, czy będzie to podejście klasyczne czy TDD. Musisz pamiętać, żeby dobierać narzędzia oraz techniki do indywidualnych potrzeb projektu.