TDD

Testowanie

  |   5 min czytania

Fazy

Test Driven Development (TDD) jest metodyką zwinną tworzenia oprogramowania sterowaną przez proces testowy, który składa się z trzech faz: tworzenia testów, implementacji funckjonalności i refaktoryzacji, pomiędzy którymi następuje wykonanie testów jednostkowych. Dodanie nowej funkcjonalności rozpoczyna się od napisania dla niej testów jednostkowych. Testy powinny być proste, a wyniki ich przeprowadzanie negatywne (ze względu na brak zaimplementowanej funkcjonalności). Na tym etapie tworzenie testów wymaga od programisty przemyślenia interfejsu funkcjonalności co zmusza go do sprecyzowania problemu. W kolejnej fazie następuje implementacja funkcjonalności realizującej oczekiwane założenia. Implementacja nie musi być optymalna pod względem dobrych praktyk czy standardów kodu, ważne jednak, aby pozytywnie przechodziła wszystkie testy co zapewnia o tym, że dodana funkcjonalność nie narusza pozostałych elementów systemu. W ostatnim kroku jeśli jest to możliwe przeprowadzana jest refaktoryzacja i uporządkowanie kodu stworzonej funkcjonalności oraz testów. Jeśli dokonane zmiany nie powodują błędów w wykonaniu testów jednostkowych można przystąpić do kolejnej iteracji.

Fazy TDD

Zalety

Główną zaletą stosowania metodyki TDD jest wychwytywanie błędów na bardzo wczesnym etapie co w konsekwencji zgodnie z zasadą 1:10:100 znacząco zmniejsza koszty ich naprawy. Co więcej błędy wykrywane są przez autora kodu i korygowane przez niego na bieżąco dzięki czemu proces naprawy nie wymaga angażowania całego zespołu QA. Ze względu na charakterystykę TDD, która wymusza tworzenie testów przed implementacją funkcjonalności tworzony kod wydaje się być bardziej przemyślany, prosty i elegancki. Dodatkowo testy jednostkowe stworzone w ramach TDD mogą służyć jako alternatywa dla dokumentacji pokazując w jaki sposób może zostać użyta funkcjonalność.

Wady

Tworzenia oprogramowania przy pomocy metodyki TDD obarczone jest przede wszystkim zwiększonym nakładem czasowym potrzebnym do tworzenia i utrzymania testów jednostkowych. Każda nowa funkcjonalność wymaga stworzenia testów. Aby testy były użyteczne powinny być również modyfikowane wraz ze zmianą implementacji funkcjonalności. Należy mieć jednak na uwadze, że poświęcony czas może zwrócić się z nawiązką w kolejnych fazach tworzenia oprogramowania, ponieważ w znaczący sposób zapobiega pojawianiu się błędów w finalnym oprogramowaniu.

Zastosowanie

Wykorzystanie metodyki TDD ma zastosowanie tam gdzie możliwe jest otrzymanie większych korzyści w stosunku do poświęconego czasu. Podejmując decyzję o wcieleniu w życie TDD należy przeprowadzić analizę zysków i strat włączając w to wymagania spełnienia jakości. Warto również zastanowić się nad przeprowadzaniem testów dla trywialnych lub abstrakcyjnych zadań (których sposób implementacji nie jest oczywisty). TDD może nie sprawdzić się w przypadku małych projektów lub tych które nie będą rozwijane czy też w projektach już istniejących. Decydując się na tworzenie oprogramowania drogą TDD należy pamiętać, że metodyka jak każda inna ma służyć rozwijaniu oprogramowania i nie jest celem samym w sobie.

Dobre praktyki

Przechodząc przez kolejne fazy wytwarzania oprogramowania metodyką TDD należy mieć na uwadzę dobre praktyki tworzenia niezależnych, atomowych, czytelnych i krótkich testów jednostkowych. Poza tym dobrze jest znaleźć odpowiedni bilans długości, wielkości i częstotliwości faz cyklów, który wynika z charakterystyki projektu oraz osobistych preferencji programisty. Należy odpo

Przykład

Aplikacja Kalkulator ma posiadać funkcję liczenia ciągu Fibonacciego dla zadanej wartości. Programista przystępuje najpierw do napisania testów oraz naiwnej implementacji testowanej funkcjonalności tak, aby przeprowadzenie testu było możliwe i zakończyło się wynikiem negatywnym.

class CalculatorTest {

    @Test
    fun checkFibonacci() {
        assertEquals(0, Calculator.fibonacci(0))
        assertEquals(1, Calculator.fibonacci(1))
        assertEquals(8, Calculator.fibonacci(6))
    }
}

class Calculator {
    
    companion object {        
        fun fibonacci(n: Int): Int {
            return -1
        }
    }
}

Uruchomione testy dają wynik negatywny zatem programista może przejść do fazy implementacji funkcjonalności.

class Calculator {
    
    companion object {        
        fun fibonacci(n: Int): Int {
            if (n == 0)
                return 0
            else if (n == 1)
                return 1            
            else
                return fibonacci(n-1) + fibonacci(n-2)
        }
    }
}

Po zakończonej fazie implementacji i pozytywnym przejściu wszystkich testów może nastąpić faza refactoringu. Programista dostrzega możliwość poprawienia kodu funkcjonalności o przyjmowany i zwracany typ argumentu oraz poprawienie wydajności. Ponadto zwraca uwagę na potrzebę dodania dodatkowych asercji w testach.

class Calculator {

    companion object {
        tailrec fun fibonacci(n: Int, a: BigInteger = BigInteger.ZERO, b: BigInteger = BigInteger.ONE): BigInteger {
            return if (n == 0)
                a
            else
                fibonacci(n-1, b, a+b)
        }
    }
}

class CalculatorTest {

    @Test
    fun checkFibonacci() {
        assertEquals(BigInteger.valueOf(0), Calculator.fibonacci(0))
        assertEquals(BigInteger.valueOf(1), Calculator.fibonacci(1))
        assertEquals(BigInteger.valueOf(1), Calculator.fibonacci(2))
        assertEquals(BigInteger.valueOf(8), Calculator.fibonacci(6))
    }

    @Test(expected = java.lang.IllegalArgumentException::class)
    fun checkFibonacciExceptionForMaxNegativeArg() {
        fibonacci(-1)
    }

    @Test(expected = java.lang.IllegalArgumentException::class)
    fun checkFibonacciExceptionForMinNegativeArg() {
        fibonacci(Int.MIN_VALUE)
    }

    //add more test for proper max arg value
}

Przeprowadzone testy dla zmodyfikowanego kodu nadal dają wynik pozytywny. Programista może zatem uznać pełen cykl implementacji funkcji ciągu Fibonacciego za ukończony i przejśc do implementacji kolejnych funkcjonalności aplikacji Kalkulator.