Hlavní navigace

Test-driven development v jazyce Java

10. 1. 2013
Doba čtení: 5 minut

Sdílet

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:

root_podpora

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.

Byl pro vás článek přínosný?