Testy jednostkowe: Technika TDD (Test-Driven Development)

TDD, czyli Test-Driven Development

TDD (ang. Test-Driven Development) jest techniką tworzenia oprogramowania, w której główną zasadą jest w pierwszej kolejności pisanie testów do nieistniejącej funkcjonalności, a następnie jej zaimplementowanie.
Tworząc oprogramowanie zgodnie z techniką TDD testy piszemy zawsze przed implementacją.
Podejście TDD składa się z trzech faz, które łączą się w jeden cykl – cały proces tworzenia oprogramowania składa się z takich właśnie cykli powtarzanych jeden po drugim.

Wyróżniamy trzy fazy cyklu TDD: red, green, refactor.

Źródło: https://www.bdabek.pl/

Faza Red
Pierwszym krokiem tworzenia kodu według metodyki TDD jest napisanie testu – piszemy test, który nie skompiluje się, ponieważ funkcjonalność nie jest jeszcze zaimplementowana.

Faza Green
Kolejnym krokiem jest napisanie minimalnej ilości kodu, który zaimplementuje brakującą funkcjonalność – test skompiluje się.

Faza Refactor
Jest to ostatnia faza, w której dokonujemy refaktoryzacji kodu – ulepszamy jakość i czytelność kodu.

Przykład użycia techniki TDD

Naszym zadaniem jest stworzyć funkcjonalność, mającą na celu wymnożenie dwóch liczb.
Zgodnie z TDD zaczniemy od fazy Red, czyli od stworzenia nieprzechodzącego testu – klasa  Multiplication jeszcze nie istnieje, dlatego test nie skompiluje się.

import org.junit.jupiter.api.Test;
 
public class MultiplicationTest {
 
    @Test
    void shouldMultiplyTwoNumbers(){
        
        //given
        Multiplication multiplication = new Multiplication();
    }
} 

Kolejnym krokiem będzie napisanie minimalnej ilości kodu, który spowoduje, że nasz test skompiluje się.

public class Multiplication {
} 

Według TDD kolejnym krokiem powinna być refaktoryzacja, jednak my na ten moment nie mamy nic do ulepszenia. Dodajemy więc do testu wywołanie funkcji, która przemnoży przez siebie dwie zadane liczby.

import org.junit.jupiter.api.Test;
 
public class MultiplicationTest {
 
    @Test
    void shouldMultiplyTwoNumbers() {
        
        //given
        Multiplication multiplication = new Multiplication();
        
        //when
        int multiplicationResult = multiplication.multiplyNumbers(2,4);
    }
} 

Ponownie test nie przechodzi, ponieważ brakuje implementacji metody multiplyNumbers – dodajmy zatem ją do klasy Multiplication.

public class Multiplication {
    public int multiplyNumbers(int number1, int number2) {
    }
} 

Po refaktoryzacji, po raz trzeci zaczynamy cykl TDD od początku – tworzymy odpowiednią asercję, która sprawdzi czy wynik mnożenia jest prawidłowy.

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
 
public class MultiplicationTest {
 
    @Test
    void shouldMultiplyTwoNumbers() {
        
        //given
        Multiplication multiplication = new Multiplication();
        
        //when
        int multiplicationResult = multiplication.multiplyNumbers(2,4);
        
        //then
        assertEquals(multiplicationResult, 8);
    }
} 

Asercja oczywiście nie zostaje spełniona (faza Red) ponieważ nadal nie zaimplementowaliśmy metody multiplyNumbers. Dodajemy więc odpowiedni kod do naszej metody przechodząc do etapu Green.

public class Multiplication {
    public int multiplyNumbers(int number1, int number2) {
        return number1 * number2;
    }
} 

Po ostatnim etapie refaktoryzacji, nasza funkcjonalność działa w pełni prawidłowo oraz została stworzona zgodnie z techniką TDD.

Dobre praktyki pisania testów

 

Reguły TDD FIRST:

F – fast / szybkie – wykonanie testu powinno trwać średnio kilka – kilkanaście ms na jeden test,
I – independent (isolated) / niezależne – odizolowanie testowanego kodu od operacji na bazach danych czy systemów plików oraz od innych testów, tak aby dany test nie polegał na ich wynikach,
R – repeatable / powtarzalne – powinny dawać ten sam wynik przy wielokrotnym uruchomieniu testu),
S – self-checking / samokontrola – automatyzacja uruchamiania testów),
T – timely / aktualne – pisane razem z kodem produkcyjnym).

Reguły TDD CORRECT:

C – conformance / zgodność – zgodność zarówno z wymaganiami biznesowymi jak i zgodność wartości tekstowych z ich formatem (np. sprawdzenie czy adres email ma odpowiedni format),
O – ordering / porządek danych – jeśli metoda na wejściu oczekuje listy posortowanej, powinniśmy przetestować również przypadek jak zachowa się gdy otrzyma listę nieposortowaną,
R – range / zakres – sprawdzanie przekroczenie maksymalnej wartości dla danego typu (np. int) oraz sprawdzanie rozsądnego zakresu zmiennej (np. wiek osoby),
R – reference / odniesienie – sprawdzenie czy wszystkie warunki wstępne zostały wykonane (given, when, then),
E – existence / istnienie – co stanie się jeśli do metody jako parametr prześlemy wartość pustą, null lub zero itp.
C – cardinality / moc zbioru – sprawdzenie ilości elementów zbioru, np. po wykonaniu jakiejś metody,
T – time / czas – sprawdzenie kolejności wywołania metod oraz testowanie wielowątkowych miejsc w kodzie.

 

Leave a Comment

Your email address will not be published. Required fields are marked *