#Niezbędnik Juniora: test-driven development

By 10 stycznia 2016Niezbędnik Juniora

Testowanie swo­jego kodu to bard­zo istot­na część pra­cy każdego pro­gramisty, jed­nak, nie każdy z nas lubi to robić. Sposobem na zapewnie­nie odpowied­niego pokrycia swo­jego kodu może być TDD.

Czym jest TDD?

To nic innego jak pisanie opro­gramowa­nia sty­mu­lowane przez testy. Cykl imple­men­tacji nowej funkcjon­al­noś­ci wyglą­da następu­ją­co:

1. Dodaj test

Dodanie każdej nowej funkcjon­al­noś­ci rozpoczy­na się od napisa­nia tes­tu. Aby być w stanie napisać test musisz dobrze rozu­mieć imple­men­towaną funkcjon­al­ność — jej wyma­gania i wynik dzi­ała­nia — rozbi­ja­jąc to na pojedyńcze cechy (tak jak w tes­tach jed­nos­tkowych). Możesz napisać od razu wszys­tkie testy, lub tylko jeden, ważne by w tym etapie w ogóle nie myśleć o kodzie, który będzie im odpowiadał — masz po pros­tu zmapować oczeki­wane zachowanie funkcjon­al­noś­ci.

2. Uruchom test(y)

Powin­ny one nie prze­chodz­ić. Każdy nowododany test nie może zakończyć się sukce­sem — gdy­by tak było, był­by on z bard­zo dużym praw­dopodobieńst­wem napisany w zły sposób. Pamię­taj, że taki test powinien nie prze­jść z znanych Ci powodów — w kole­jnym kroku będziesz musi­ała bowiem dodać kod, który poz­woli mu zakończyć się sukce­sem.

3. Napisz kod

Dodaj kod, dzię­ki które­mu test prze­jdzie pozy­ty­wnie. Kod ten nie musi być per­fek­cyjny (na refac­tor będzie czas później), nato­mi­ast nie powinien on wyprzedzać napisanych testów. Dodatkowe funkcjon­al­noś­ci powin­ny być imple­men­towane dopiero kiedy pojaw­ią się nowe testy.

4. Uruchom testy

W rezulta­cie wszys­tkie doty­chczas niedzi­ała­jące testy powin­ny prze­chodz­ić pozy­ty­wnie (jeśli nie — wracasz do pro­gramowa­nia). Jeśli wszys­tkie napisane testy kończą się sukce­sem, oznacza to, że Twój kod odpowia­da wyma­gan­iom biz­ne­sowym.

5. Refactor kodu

To bard­zo ważny ele­ment takiego pode­jś­cia do imple­men­tacji — jako, że TDD pozwala na chwilowe rozwiąza­nia (o tym za chwile w przykładzie), czyszcze­nie kodu jest ważnym i potrzeb­nym etapem. Wszys­tkie powtórzenia powin­ny być wye­lim­i­nowane, a kod odpowied­nio uporząd­kowany — jak?- o tym przeczy­tasz w naszym wpisie o refak­torze.

Powtórz

Aż do dostar­czenia pełnej funkcjon­al­noś­ci. Pamię­taj, że z TDD możesz korzys­tać również gdy mody­fiku­jesz już napisaną aplikację bądź pracu­jesz z lega­cy code.

Przykład

Wyobraźmy sobie, że mamy do napisa­nia aplikację, która oblicza sil­nię. Poniższy przykład będzie w pseu­do kodzie:

W pier­wszej iter­acji napisałabym test, który sprawdza, czy pro­gram poprawnie obliczył 0!

//Test
int liczba = 0;
AssertEquals(0,obliczSilnie(liczba));

Odpowiedz­ią na taki kod, była by meto­da przyj­mu­ją­ca int:

obliczSilnie (int liczba){
    return 1;
}

Jak widzi­cie, na ten moment nie robię tego w praw­idłowy sposób, po pros­tu odpowiadam na test, tak by prze­chodz­ił.
Pisząc test dla 1! okaza­ło­by się, że nie muszę nic mody­fikować w kodzie.
Dla 2! mogłabym np. napisać coś takiego:

obliczSilnie (int liczba){
    if (liczba == 0)
        return 0;
    else
        return liczba;
}

Ter­az wszys­tkie 3 testy mi prze­chodzą, ale nadal nie korzys­tam z metody oblicza­nia sil­ni — powin­nam to zmienić, z resztą test case dla 3! to na mnie wymusza.
Mody­fiku­je więc mój kod:

obliczSilnie (int liczba){
    if (liczba == 1)
        return 1;
    else
        return liczba * obliczSilnie(liczba - 1);
}

Mam więc praw­ie gotowy kod (braku­je tu wery­fikacji czy nasz int jest liczbą nat­u­ral­ną), a dos­zliśmy do niego pisząc 4 przy­pad­ki testowe. 

Kiedy stosować?

Po pier­wsze, kiedy jesteś na początku swo­jej nau­ki pro­gramowa­nia i nie do koń­ca opanowałeś testy — wtedy takie pode­jś­cie zmo­ty­wu­je Cię do szyb­szego ich opanowa­nia — to naprawdę super sposób na wyćwicze­nie się w pisa­niu testów!

Po drugie, gdy nie do koń­ca wiesz jak zaim­ple­men­tować daną funkcjon­al­ność — TDD sku­pia się na dokład­nym zrozu­mie­niu wyma­gań i potrzeb klien­ta, a także zmusza Cię do rozbi­cia funkcjon­al­noś­ci na małe kro­ki — dzię­ki takiemu pode­jś­ciu łatwiej będzie Ci znaleźć rozwiązanie.

Po trze­cie, kiedy mody­fiku­jesz już dzi­ała­jące funkcjon­al­noś­ci takie pode­jś­cie poz­woli Ci łatwiej zwery­fikować koniecznie mody­fikac­je.

Uwaga!

TDD jako meto­da pro­gramowa­nia ma tyle samo zwolen­ników co prze­ci­wników — czy po pros­tu osób, które nie lubią w taki sposób pra­cow­ać. Nie należy z test dri­ven devel­op­ment robić religii — jedynego słusznego pode­jś­cia. Warto na pewno poz­nać taki sposób pra­cy — jeśli będzie Ci on odpowiadał możesz stosować go na codzień, a jeśli nie — wiedzieć, że możesz go wyko­rzys­tać w trud­nych przy­pad­kach by lep­iej zrozu­mieć wyma­gania funkcjon­al­noś­ci.

Dodatkowa literatura:

 

  •  
  •  
  •  
  • 5
  •  
  • Paweł

    Czy w ostat­nim listin­gu w piątej lin­i­jce nie powin­no być “obliczSil­nie” zami­ast “sil­nia”? Poz­draw­iam, świet­ny blog !
    Paweł

    • Fak­ty­cznie, umknęło nam to, dzię­ki! Już popraw­ione :)

  • Dmytro

    if (i = 1)???

    • frag­men­ty kodu we wpisie należy trak­tować bardziej jako pseudokod niż konkret­ny język, chodz­iło przede wszys­tkim o pokazanie idei. Popraw­iliśmy jed­nak, aby było spójnie ;)

  • str

    obliczSil­nie (int licz­ba){
    if (i == 1)
    return 1;
    else
    return i * obliczSilnie(i — 1);
    }
    Powin­no być licz­ba zami­ast i.
    Wypadało­by też dodać klam­ry do ifa.

    • Poniższy przykład będzie w pseu­do kodzie’

      • Ewa Śli­wińs­ka

        Nie wiem jaki jest sens uży­wa­nia dwóch różnych nazw dla tej samej zmi­en­nej w pseudokodzie.