TDD – Test-driven development

Czym jest TDD?

Test-driven development (TDD) to sposób produkcji oprogramowania, którego główną zasadą jest pisanie testów przed napisaniem właściwego kodu. W skrócie proces tworzenia możemy zawrzeć w trzech krokach:

  • Napisanie testu
  • Zaprogramowanie funkcji, która przechodzi test
  • Refactoring kodu

Podejście takie zapewnia nam przede wszystkim maksymalne pokrycie kodu testami. Co warto podkreślić, maksymalne, nie oznacza 100%, ponieważ nie wszystkie metody warto testować (jak np. proste gettery czy setter), nastawienie na sprawdzanie każdej linijki kodu jest błędne.

Red – green – refactor

Powyższy diagram przedstawia cykl pracy przy wykorzystaniu metodyki TDD. Pisanie zaczynamy od testu jednostkowego, który „nie przechodzi”. Jest on reprezentacja tego jak funkcjonalność ma działać w przyszłości, zbiorem oczekiwanego zachowania. Kolejnym krokiem jest napisanie kodu, który uprzednio napisany test przechodzi. Nie należy tutaj kłaść nacisku na optymalne rozwiązanie problemu, a na jego rozwiązanie w ogóle (zachowując zdrowy rozsądek oczywiście), ponieważ tym zajmiemy się w kolejnym etapie, którym jest refactoring. Wspomniany wcześniej refactoring występuje na końcu cyklu, dopiero tutaj musimy przysiąść nad kodem i opracować go tak, aby spełniał określone standardy.

Istotną kwestią jest, aby wyciągnąć ze wspomnianego diagramu informację, iż każdy krok jest równie ważny i nie wolno lekceważyć żadnego z nich, przy czym należy pamiętać o kolejności red – green – refactor. Kolejną niemniej istotną sprawą jest pisanie kodu w możliwie małych sekcjach. Zaczynamy od pojedynczych testów, następnie piszemy tylko taką ilość kodu, który przychodzi ten test i proces powtarzamy.

TDD na przykładzie

Na poniższym, bardzo prostym przykładzie, zostaną omówione podstawowe założenia TDD. Załóżmy, że mamy klasę Unit, przedstawiającą jednostkę w grze, z trzema polami hitPoints, attack oraz speed. Chcemy utworzyć dla niej klasę nadrzędną UnitRepository, która będzie zarządzała jednostkami posiadanymi przez danego gracza. Stworzymy ją wykorzystując podejście TDD.

public class Unit {

    private int hitPoints;
    private int attack;
    private int speed;

    public Unit(int hitPoints, int attack, int speed) {
        this.hitPoints = hitPoints;
        this.attack = attack;
        this.speed = speed;
    }

    public int getHitPoints() {
        return hitPoints;
    }

    public int getAttack() {
        return attack;
    }

    public int getSpeed() {
        return speed;
    }
}

Zgodnie z TDD rozpoczynamy od stworzenia pierwszego testu. Na początku chcielibyśmy mieć możliwość dodawania jednostek do repozytorium. Piszemy więc test.

class UnitsRepositoryTest {

    @Test
    void shouldBeAbleToAddUnitsToRepository(){
    //given
        UnitRepository unitRepository = new UnitRepository();
    }
}

I tak naprawdę już w tym momencie otrzymujemy test nieprzechodzący (red), ponieważ nie stworzyliśmy jeszcze klasy UnitRepository. Zabieramy się więc za jej stworzenie i uruchamiamy test. Tutaj trzeba zaznaczyć, że powinniśmy się kierować zasadą, która mówi, że piszemy tylko wystarczającą ilość kodu do zaliczenia testu, co w naszym przypadku oznacza stworzenie pustej klasy UnitRepository.

public class UnitRepository {

}

Uruchamiamy test i jak się okazuje, zostaje on zaliczony (green). W tym momencie cykl się zakończył, więc przechodzimy do nowej iteracji. Jako, że metoda ma docelowo dodawać jednostkę do repozytorium, tworzymy kolejny test:

class UnitsRepositoryTest {

    @Test
    void shouldBeAbleToAddUnitsToRepository(){
    //given
        UnitRepository unitRepository = new UnitRepository();
        Unit unit = new Unit(5,8,20);
        
    //when
        UnitRepository.addUnitToRepository(unit);
    }
}

Test taki już nie przechodzi, ponieważ nie mamy zaimplementowanej metody addUnitToRepository. Zajmijmy się jej implementacją. Finalnie, po kilku kolejnych iteracjach, klasy jakie otrzymaliśmy będą wyglądały następująco:

class UnitsRepositoryTest {

    @Test
    void shouldBeAbleToAddUnitsToRepository(){
        //given
        UnitRepository unitRepository = new UnitRepository();
        Unit unit = new Unit(5,8,20);

        //when
        unitRepository.addUnitToRepository(unit);

        //then
        assertThat(unitRepository.getAllUnits().get(0), is(unit));
    }
}

public class UnitRepository {

    private final List<Unit> unitsList = new ArrayList<>();

    public void addUnitToRepository(Unit unit) {
        unitsList.add(unit);
    }

    public List<Unit> getAllUnits(){
        return unitsList;
    }
}

Warto mieć na uwadze, że w tak prostym przykładzie, nie zachodzi potrzeba rafactoringu kodu, jednak przy bardziej skomplikowanych funkcjonalnościach, zawsze po etapie green należy kod ustandaryzować według stosowanych w zespole reguł. Pamiętajmy również, że refactoring tyczy się również kodu testowego o który należy dbać tak samo jak o logikę biznesową.

Czy to się opłaca?

Na pierwszy rzut oka może się wydawać, że podejście TDD jest bardzo czasochłonne, wiąże się z pisaniem wielu testów, które niejednokrotnie są dłuższe niż kod samej logiki biznesowej. Dochodzi dodatkowo obsługa i utrzymanie dodatkowych funkcji testujących. Jednak te niedogodności okazują się dużo mniej istotnie jeśli przyrównać je do korzyści jakie wynikają z TDD. Przy kierowaniu się tą metodyką mamy zawsze zapewnione dobre pokrycie testami, co daje kolejne profity jak łatwe modyfikacje, rozszerzalność czy stabilność działania aplikacji. Programista, który pracuje z tak napisanym kodem nie boi się dokonywać zmian, ulepszać, bo od razu ma informację zwrotną czy jego pomysły i rozwiązania działają tak jak to zaplanował.

Iteracyjność TDD daje również na pozór niewidoczną zaletę. Praca podzielona jest na krótkie cykle „nie działa – działa”, co daje programiście większą satysfakcję z uwagi na małe, aczkolwiek często odnoszone sukcesy.

Podsumowanie

Jak każde rozwiązanie, TDD ma wady i zalety. Decyzja o zastosowaniu tej metodyki powinna zostać podjęta świadomie i zależeć od rodzaju i rozmiaru projektu. Przy większych, koszt ten zwraca się z nawiązką w późniejszych etapach. Szczerze polecam zainteresować się tematyką TDD i mam nadzieję, że udało mi się na tyle dobrze zaprezentować to podejście, aby zachęcić Cię do spróbowania swoich sił i wdrożenia tej metodologii w swoim projekcie!

Dodaj komentarz