Test-driven development v jazyce Java

Vlastimil Pospíchal 10. 1. 2013

Test-driven development (dále jen TDD) neboli programování řízené testy se používá k ladění jednotlivých modulů aplikací. Pokud si vyhledáme články o TDD, zpravidla nalezneme popis nějaké knihovny JUnit pro Javu nebo PHPunit pro PHP. Přitom je možné začít programovat stylem TDD i bez použití těchto nástrojů.

K čemu je TDD vlastně dobré?

Žádný programátor nepíše bez chyb. Před ostrým nasazením programu si ho minimálně jednou musí spustit, aby zjistil, zda program dělá vše, co je od něj požadováno. Jak praví folklór, v každém programu je minimálně jedna chyba. Některé chyby odhalí kompilátor, ale mnoho jich kompilací projde. Projevují se neočekávaným chováním programu. Programátor tedy vykonává periodickou činnost stylem oprava – kompilace – testování. Pokud bychom tento cyklus chtěli automatizovat, tak na opravy programu kromě lepšího editoru nic nepomůže a kompilátor už asi také nevylepšíme. Zbývají testy.

Ano, testy. Můžeme je dělat ručně, ale pokud je budeme dělat stále dokola, bude nás to zdržovat. Proto si na ně napíšeme program. Nemusí být nijak estetický a ani nemusí být ve stejném jazyku, ve kterém bude výsledná aplikace. Často se pro testování používá skriptovací jazyk, i když výsledná aplikace je kompilovaná.

Chceme testovat v Javě. Jak na to?

Programovací jazyk Java od verze 1.4 nabízí přímo prostředky pro ladění programů. Stačí použít klíčové slovo assert s jednoduchou syntaxí:

assert podmínka : "chybové hlášení";

které znamená: „Pokud není splněna podmínka, vypiš chybové hlášení a přeruš vykonávání programu.“ Chování je tedy obráceně, než jak je to u podmínky if. Chybové hlášení (včetně dvojtečky) je nepovinné, příkaz přesto v případě nepravdivosti podmínky zahlásí místo v programu, kde k chybě došlo. Výpis je tedy pro ladění mnohem užitečnější, než System.err.writeln(). Ve zkrácené verzi tedy stačí jen:

assert podmínka;

Ve skutečnosti program nemusí být ukončen. Je vyvolána výjimka AssertionError, která je potomkem třídy Error. Není nutné ji ošetřovat a zpravidla to ani není vhodné. Je jen dobré vědět, že je to možné.

Jednoduchý příklad

Potřebujeme třídu, která nám bude uchovávat souřadnice [x,y] v objektu a dokáže spočítat vzdálenost mezi dvěma objekty na ploše.

Nejprve napíšeme základ testu

Zní to podivně, ale ještě před prvním písmenkem programu začneme psát test. Co do něj napíšeme? Zadání vyjádřené ve výrazech. Je zvyklostí testy pojmenovávat s předponou „test“. Nejprve si napíšeme základ testu. Osobně jsem si tento základ vložil do šablony, takže se mi objeví pokaždé, když založím nový soubor.

class testPoint {
    public static void main(String[] args) {
        boolean assertsEnabled = false;
        assert assertsEnabled = true;
        if (!assertsEnabled)
            throw new RuntimeException("Asserty se zapínají parametrem -ea");
    }
}

Třída a v ní jedna metoda main. Uvnitř jsou čtyři trochu podivné řádky. Jsou tam proto, abych nezapomněl při spuštění testu povolit asserty. Pokud bych na to zapomněl, mohl bych si myslet, že testy proběhly v pořádku.

Za tento základ nyní napíšeme první test

class testPoint {
    public static void main(String[] args) {
        boolean assertsEnabled = false;
        assert assertsEnabled = true;
        if (!assertsEnabled)
            throw new RuntimeException("Assert se zapíná parametrem -ea");
        Point a = new Point(1.0, 2.0);
        Point b = new Point(4.0, 6.0);
    }
}

Vytvoříme dvě instance třídy Point. Konstruktoru budeme chtít předat parametry [x,y]. Soubor uložíme a necháme zkompilovat. Dle očekávání kompilátor ohlásí chybu:

testPoint.java:12: cannot find symbol
symbol  : class Point
location: class testPoint

Mohlo by se stát, že by chybu nezahlásil?

Ano, mohlo. Mohl by najít nějakou existující třídu s názvem Point a pokusil by se ji použít. Možná by nám i vyhovovala a mohli bychom se rozhodnout, že je pro naši úlohu vhodná, ale to teď pomiňme a předpokládejme, že třída Point zatím neexistuje. Je tedy nutné ji napsat.

class Point {
    private double x, y;

        public Point(double x, double y) {
            this.x = x;
            this.y = y;
        }
}

Nyní spustíme test

Skvělé, test proběhl OK. Jenže třída zatím skoro nic nedělá. Proč nic nedělá? Protože jsme od ní v testu skoro nic nepožadovali. Rozšíříme tedy požadavky:

class testPoint {
    public static void main(String[] args) {
        boolean assertsEnabled = false;
        assert assertsEnabled = true;
        if (!assertsEnabled)
            throw new RuntimeException("Assert se zapíná parametrem -ea");

            Point a = new Point(1.0, 2.0);
            Point b = new Point(4.0, 6.0);
        assert 5.0 == a.diff(b) : "Očekávám výsledek 5.0, ale vyšlo " + a.diff(b);
    }
}

Opět spustíme test

Už při kompilaci testu opět dostaneme chybové hlášení, že nám tato metoda chybí.

javac testPoint.java
testPoint.java:14: cannot find symbol
symbol  : method diff(Point)

Tak ji dopíšeme

class Point {
    private double x, y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double diff(Point druhy) {
        return 5.0;
    }
}

Znovu spustíme test

Nyní proběhne OK. Jenže všichni vidíme, že jsem toho dosáhl podfukem. Proto platí zásada: „Nikdy nevěřte testům, které neselžou.“ Upravíme tedy testy:

class testPoint {
    public static void main(String[] args) {
        boolean assertsEnabled = false;
        assert assertsEnabled = true;
        if (!assertsEnabled)
            throw new RuntimeException("Assert se zapíná parametrem -ea");

        Point a = new Point(1.0, 2.0);
        Point b = new Point(4.0, 6.0);
        assert 5.0 == a.diff(b) : "Očekávám výsledek 5.0, ale vyšlo "
+ a.diff(b);

        Point c = new Point(6.0, 14.0);
        assert 13.0 == a.diff(c) : "Očekávám výsledek 13.0, ale vyšlo
" + a.diff(c);
    }
}

Je možné přidat i víc, ale tohle nám v tuto chvíli bude stačit. Překladač žádnou chybu nehlásí, ale je asi jasné, že takový test neprojde:

Exception in thread "main" java.lang.AssertionError: Očekávám výsledek 13.0, ale vyšlo 5.0

Bylo by dobré si přestat hrát s konstantou a napsat správně funkci na výpočet vzdálenosti:

class Point {
    private double x, y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double diff(Point druhy) {
        double x = this.x - druhy.x;
        double y = this.y - druhy.y;
        return Math.sqrt(x*x + y*y);
    }
}

Hotovo

Výborně, vše pracuje přesně jak má. Dle vlastního uvážení opět dopíšeme do testu několik assertů a znovu spustíme test. Pokud je vše v pořádku, jsme hotovi. Testy nemažeme, uchováme si je pro případy upgrade částí systému, běhového prostředí a jiných změn, o kterých předem nevíme. Pokud obdržíme od klienta hlášení, že program má neobvyklé chování pro určité kombinace vstupů, přidáme je do testu. Pokud bude chtít další metody, opět nejprve přidáme do testu jejich volání, spustíme test a podle výsledku testu dopíšeme a otestujeme chybějící část kódu.

Našli jste v článku chybu?
Vitalia.cz: Klíšťata letos řádí, skvrna se udělá jen někomu

Klíšťata letos řádí, skvrna se udělá jen někomu

120na80.cz: I tuto vodu můžete pít

I tuto vodu můžete pít

Podnikatel.cz: Pohlaví, věk, víra a další. Při pohovoru tabu

Pohlaví, věk, víra a další. Při pohovoru tabu

Lupa.cz: Vodafone umí volání přes Wi-Fi. Z ciziny jako v ČR

Vodafone umí volání přes Wi-Fi. Z ciziny jako v ČR

Vitalia.cz: Signál roztroušené sklerózy: brnění končetin

Signál roztroušené sklerózy: brnění končetin

Vitalia.cz: Cheese&Chilli: předsudky o nudné britské kuchyni

Cheese&Chilli: předsudky o nudné britské kuchyni

Měšec.cz: Kurzy platebních karet: vyplatí se platit? (TEST)

Kurzy platebních karet: vyplatí se platit? (TEST)

Měšec.cz: Do ostravské MHD bez jízdenky. Stačí vaše karta

Do ostravské MHD bez jízdenky. Stačí vaše karta

Podnikatel.cz: Účtenky v rámci EET? Klidně emailem

Účtenky v rámci EET? Klidně emailem

Vitalia.cz: Bio vejce nepoznají ani veterináři

Bio vejce nepoznají ani veterináři

Měšec.cz: Co s reklamací, když e-shop krachuje?

Co s reklamací, když e-shop krachuje?

Podnikatel.cz: Nereaguje na výzvu ČOIky, zaplatí milion

Nereaguje na výzvu ČOIky, zaplatí milion

Lupa.cz: eIDAS: Nepřehnali jsme to s výjimkami?

eIDAS: Nepřehnali jsme to s výjimkami?

Podnikatel.cz: Fotogalerie: Jesenka už má skoro 50 let

Fotogalerie: Jesenka už má skoro 50 let

Lupa.cz: V Praze se otevřel první podnik s virtuální realitou

V Praze se otevřel první podnik s virtuální realitou

Měšec.cz: Investice do drahých kovů - znáte základní chyby?

Investice do drahých kovů - znáte základní chyby?

Vitalia.cz: Sobotní masakr žrádla, chlastu a zábavy

Sobotní masakr žrádla, chlastu a zábavy

Měšec.cz: Ceny PHM v Evropě. Finty na úspory

Ceny PHM v Evropě. Finty na úspory

DigiZone.cz: Kauza technik: oficiální vyjádření Novy

Kauza technik: oficiální vyjádření Novy

Podnikatel.cz: Polská vejce na českém pultu Albertu

Polská vejce na českém pultu Albertu