TDD – technika potrzebna programiście

TDD – technika potrzebna programiście

Gdy rozpoczynamy naszą przygodę z programowaniem często poprawność naszego kodu sprawdza słynna funkcja print(). Obserwujemy, czy na pewno metoda zwraca oczekiwaną przez nas wartość, jeśli operacja zakończy się powodzeniem uznajemy pełną poprawność implementacji i idziemy dalej. Prawdą jednak jest, że to bardzo złe rozwiązanie i często później się okazuje błędny wynik już w trakcie działania programu.

Ponadto programiści często unikają planowania ich kodu. Obserwując społeczeństwo deweloperskie spostrzegłem, że zdecydowana większość siada do kodu i od razu na żywca implementuje ich rozwiązanie. Kod jest jedną wielką bańką nieplanowanego syfu, który prawdę mówiąc nie musiał być w ogóle zapisany.

Istnieje jednak wspaniałe rozwiązanie – Test Driven Development, w skrócie TDD. Jest to technika programowania, która opisuje sposób pisania kodu i budowy oprogramowania. Jej głównym celem jest:

  • utworzenie wysokiej jakości oprogramowania
  • dobre zaplanowanie składowych klas, czy funkcji
  • zachowanie prostoty w kodzie

Mówiąc o prostocie w kodzie mam na myśli reguły wytwarzania aplikacji

  • KISS – Keep It Simple Stupid, która mówi o maksymalnie banalnej implementacji, takiej żeby przeciętny Kowalski mógł opanować nasz pomysł
  • YAGNI – You aren’t gonna need it. Podczas tworzenia projektu dopisujemy linijki, które są kompletnie niepotrzebne i nie będziemy z nich korzystać. Reguła mówi, żeby wytwarzać jedynie użyteczny kod.

Dzięki takim założeniom TDD jest świetnym pomysłem, który pozwala na utrzymanie wysokiego poziomu kodu oraz zapewnia jego czytelnikom całkiem łatwe wprowadzenie do projektu.

Na czym polega TDD?

Poniższy obrazek w idealnym stopniu ukazuje schemat TDD:

tdd timeline

Wytwarzanie oprogramowania dzielimy na trzy fazy – czerwoną, zieloną i niebieską, gdzie:

  • czerwona oznacza napisanie testów z wykorzystaniem metod, które zamierzamy zaimplementować. Oczywiście testy te nie zdadzą egzaminów, ponieważ ich nie ma.
  • zielona mówi o napisanym kodzie, który przechodzi wspomniane testy. Oprogramowanie działa i wiemy, że pomimo nieoptymalnego rozwiązania kod śmiga.
  • niebieska, która nazywana jest również fazą refaktoryzacji. Klientowi nie oddamy przecież takiego brzydkiego i nieoptymalnego kodu, trzeba poprawić co niektóre funkcjonalności i zapewnić najwyższą jakość zaimplementowanych metod.

Kluczowym aspektem TDD jest pisanie testu przed napisaniem kodu, który ma ten test zdać. Takie wymuszenie jest kluczowe, dzięki temu zapewnimy dobry design projektu oraz zaplanujemy metody klas i funkcje. Warto dodać, że testy pisane w TDD są tylko i wyłącznie testami jednostkowymi.

Strukturę testu jednostkowego definiuje AAA (Arrange Act Assert), gdzie:

  • arrange mówi o zapisaniu wszystkich danych wejściowych i warunków początkowych
  • act opowiada się, za działaniem metody / funkcji / klasie testowanej.
  • assert upewnia się, że wszystkie zwrócone dane są zgodne z oczekiwanymi.

Inne rodzaje testów

Myślę, że warto wspomnieć jakie inne rodzaje testów możemy spotkać podczas wytwarzania oprogramowania:

  • jednostkowe – testujemy pojedynczą, jednostkową część kodu
  • integracyjne – testujemy kilka komponentów jednocześnie
  • regresyjne – testowanie całego systemu wraz z naszym dopisanym kodem
  • akceptacyjne – testowanie sprawdzające, czy projekt spełnia wymagania biznesowe

Najważniejsze zasady pisania testów jednostkowych

  • powinny uruchamiać się poniżej 1s
  • jeden test nie powinien uruchamiać innego testu
  • nie mogą mieć stanów początkowych ani zasobów do wyczyszczenia
  • zawsze zwracają ten sam wynik
  • nie może istnieć przypadek, że test przeszedł, a kod nie
  • każdy test powinien mieć przynajmniej jedną asercję
  • zasada pojedynczej odpowiedzialności – jeden test sprawdza jedną logiczną asercję
  • kod testowy jest równie ważny co kod produkcyjny
  • nie powinny zawierać odnośników do konsoli systemowej

Ponadto spotkałem się z ciekawym założeniem TDD FIRST, gdzie:

  • F – fast / szybkie
  • I – independent / niezależne
  • R – repeatable / powtarzalne
  • S – self-checking / test przeszedł lub nie
  • T – timely / pisane razem z kodem produkcyjnym

Przykład pisania testów TDD w Python

Zawsze trzeba zarzucić odrobinę teorii, żeby zainteresować potencjalnego użytkownika, jednak to praktyka może wnieść najwięcej. Przejdźmy zatem do rzeczy.

Jest bardzo dużo bibliotek do testowania oprogramowania, ja skorzystam z unittest, która jest jedną z najbardziej podstawowych i banalnie prostych. Moim zdaniem bardzo dobra jeśli chodzi o początek przygody z testami jednostkowymi.

Faza czerwona

Powiedzmy, że chciałbym napisać kalkulator, który ma dodawać dwie podane mu liczby, oraz dzielić dwie podane mu liczby. W takim celu napiszemy dwa testy jednostkowe – pierwszy sprawdzający, czy metoda dodawania działa poprawnie oraz drugi, który sprawdzi czy operacja dzielenia zwraca dobry wynik.

Utwórzmy zatem plik test_calc.py, a w nim umieść proszę poniższy kod.

import unittest

from calc import Calculator


class CalcTestCase(unittest.TestCase):
    def test_add(self):
        calc = Calculator()

        self.assertEqual(calc.add(2, 3), 5)
        self.assertTrue(calc.add(2, 3) == 5)

        self.assertEqual(calc.add(2, 0), 0)
        self.assertTrue(calc.add(2, 0) == 0)

    def test_divide(self):
        calc = Calculator()

        self.assertEqual(calc.divide(6, 3), 2)
        self.assertTrue(calc.divide(6, 3) == 2)

        self.assertEqual(calc.divide(2, 0), 2)
        self.assertTrue(calc.divide(2, 0) == 2)

if __name__ == '__main__':
    unittest.main()

Najpierw importujemy moduł unittest pozwalający na pisanie takich testów. Następnie importuję moją klasę Calculator, który będzie zawierać dwie wspomniane metody. Następnie klasa CalcTestCase, która jest elementem kluczowym. To ona przeprowadza testy, a wiemy to dzięki temu, że dziedziczy po klasie znajdującej się w unittest TestCase. Testy same w sobie to po prostu kolejne funkcje zdefiniowane w CalcTestCase. Natomiast metoda unittest.main() rozpoczyna całą operację. Spróbujmy zatem uruchomić test.

tdd no module

Odpowiedź jest przewidywalna, co mamy testować, jak nawet nie posiadamy pliku w którym znajduję się klasa Calculator. Utworzyłem zatem plik calc.py, w którym znajduje się implementacja wspomnianej klasy oraz na zdjęciu poniżej rezultat uruchomienia testu.

class Calculator(object):
    pass
tdd 2 errors

Załączony obrazek definitywnie pokazuje, że znajdujemy się w fazie czerwonej. Nasze testy kończą się z wynikiem negatywnym. Możemy zatem przejść do implementacji metod klasy Calculator.

class Calculator(object):
    def add(self, x, y):
        z = x + y
        return z

    def divide(self, x, y):
        z = x / y
        return z

Proste operacje dodawania i dzielenia zostały napisane, możemy zatem przejść do testowania.

tdd error

Z rezultatu możemy wyczytać, że zostały uruchomione dwa testy, z których jeden nie przeszedł. Operacja dodawania działa bezbłędnie, natomiast problem znajduję się w metodzie dzielenia. W linijce 22 kodu testującego został sprawdzony przypadek dzielenia przez zero, nasza funkcja nie obsługuje takiego przypadku i został zwrócony błąd. Nic dziwnego można powiedzieć, zdarza się. Możemy jednak obsłużyć ten wyjątek.

def divide(self, x, y):
        if y == 0:
            return 0
            
        z = x / y
        return z

W przypadku, gdy będziemy chcieli dzielić przez zero zawsze zostanie ono zwrócone. Oczywiście to dosyć prozaiczny przykład, jednak interesuje nas sama idea testowania. Uruchommy zatem test ponownie.

passing

Faza zielona TDD

Sukces! Kod działa! Możemy być z siebie dumni. Tym samym przeszliśmy z fazy czerwonej do zielonej. Testy przechodzą ile razy byśmy ich nie uruchomili. Przyszedł czas na refaktoryzację. Moim pomysłem jest usunięcie zmiennej z, możemy wykonać operacje arytmetyczne w jednej linijce wraz z słowem kluczowym return.

Faza refaktoryzacji TDD

class Calculator(object):
    def add(self, x, y):
        return x + y

    def divide(self, x, y):
        if y == 0:
            return 0

        return x / y

Polecam ponownie uruchomić kod i przekonać się na własną rękę, że wszystko działa poprawnie.

Podsumowanie

W dzisiejszym wpisie udało nam się poznać sposób na pisanie dobrego, rzetelnego i efektywnego kodu. Nauczyliśmy się zasad pisania testów jednostkowych oraz uruchomiliśmy pierwszy program testowy. Myślę, że wraz z nowo zdobytą wiedzą możesz rozpocząć pisać takie testy samodzielnie i przekonać się o ich świetności.

Jeśli jesteś zainteresowany poznaniem języka Python polecam przeczytać wprowadzenie do tego języka.

Tagi: , , , , ,

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *