Test Driven Development (TDD)

Test Driven Development (TDD) to metodologia programowania. Polega na tym, że najpierw tworzy się test, a dopiero później kod, który ma ten test spełnić.

W tradycyjnym podejściu najpierw implementuje się funkcjonalność, a dopiero później testy. W Test Driven Development ta kolejność jest odwrócona. Najpierw określa się wymaganie jak aplikacja ma się zachować (test), następnie tworzy funkcjonalność, która to wymaganie spełni.

Proces iteracyjny

TDD jest procesem iteracyjnym. Składa się z trzech etapów:

Proces zaczyna się od napisania testu. Na tym etapie test zakończy się niepowodzeniem, bo funkcjonalność jeszcze nie istnieje.

Green

Następnym krokiem jest napisnie funkcjonalności, która spełni test. Celem jest napisanie minimalnej ilości kodu, która pozwoli zdać test. Nie należy pisać na zapas.

Refactor

Po spełnieniu testu można poprawić kod. Refaktorywzować należy i testy, i kod implementacji.

Procedurę powtarza się wielokrotnie. Zamiast pisać dużą część systemu naraz, rozwija się funkcjonalności krok po kroku. Przy dodawaniu każdej nowej funkcjonalności, każdym nowym wymaganiu, najpierw pisze się test, a dopiero potem kod.

Implementacja w praktyce

Załóżmy, że celem jest stworzenie sklepu internetowego. Sklep ma mieć funkcję koszyka zakupów. Koszyk ma mieć listę produktów i zwracać cenę całkowitą za produkty.

W metodologii TDD najpierw należy stworzyć test. Zakładamy, że koszyk powinien zwrócić cenę całkowitą wszystkich dodanych produktów:

class CartTest {

    @Test
    void CartShouldReturnTotalPriceOfAllItemsInside() {
        // given
        Cart cart = new Cart();
        Item item1 = new Item(15, "Shampoo");
        Item item2 = new Item(13, "Soap");

        cart.addItemToCart(item1);
        cart.addItemToOrder(item2);

        // then
        assertThat(cart.totalPrice(), is(28));
    }

Na tym etapie test nie zostanie spełniony (etap RED). Funkcjonalność (metoda totalPrice()) jeszcze nie istnieje. Implementujemy minimalną funkcjonalność, która spełni test:

public class Cart {

    private List<Items> items = new ArrayList<>();

    int totalPrice() {

        int sum = 0;
        for (Meal meal: meals) {
            sum += meal.getPrice();
        }
        return sum;
    }
}

Metoda powinna spełnić test (etap GREEN). Teraz można poprawić kod, np.:

public class Cart {

    private List<Items> items = new ArrayList<>();

    int totalPrice() {

        int sum = this.items.stream().mapToInt(item-> item.getPrice()).sum();
        return sum;
    }
}

Metoda została poprawiona (etap REFACTOR). Teraz załóżmy, że pojawiło się dodatkowe wymaganie. Po przekroczeniu wartości 1000 zł klient ma dostać rabat 10%. Proces zaczyna się od początku. Najpierw tworzymy nowy test:

class CartTest {

    @Test
    void ShouldHave5PercentDiscountIfTotalPriceOver1000() {
        // given
        Cart cart = new Cart();
        Item item1 = new Item(750, "Computer");
        Item item2 = new Item(620, "Smartphone");

        cart.addItemToCart(item1);
        cart.addItemToOrder(item2);

        // then
        assertThat(cart.totalPrice(), is(1233));
    }

Powyższy test znów nie zostanie spełniony (etap RED). Konieczna jest modyfikacja metody:

public class Cart {

    private List<Items> items = new ArrayList<>();

    int totalPrice() {

        int sum = this.items.stream().mapToInt(item-> item.getPrice()).sum();
        if (sum >= 1000) { 
            sum = 0.9 * sum; 
        }
        return sum;
    }
}

Metoda powinna spełnić drugi test (etap GREEN).

W ten sposób należy kontynuować w kolejnych iteracjach.

Zalety i wady TDD

W tradycyjnym podejściu najpierw powstaje kod, a testy są dopisywane później. TDD wymusza pisanie testów razem z kodem. Dzięki temu:

  • można łatwiej i szybciej wykrywać błędy,
  • rozwój i skalowanie aplikacji jest szybsze,
  • jest mniej błędów regresyjnych,
  • kod jest bardziej upożądkowany, nie ma niepotrzebnych funkcji,
  • rekatoryzacja jest łatwiejsza,
  • testy mogą służyć jako dokumentacja.

Jednocześnie TDD ma też wady:

  • przede wszystkim na początku kod powstaje wolniej,
  • a jeśli testy są źle napisane, to dają fałszywe złudzenie testowalności,
  • trzeba utrzymywać testy razem z kodem,
  • testowanie niektórych funkcji może być trudne.

Podsumowanie

W TDD testy są równie ważną częścią jak kod aplikacji. Bez dobrze napisanych testów nie będzie się dało dobrze napisać funkcjonalności. Testy trzeba utrzymywać i rafaktoryzować, tak jak każdy inny fragment kodu.