Válí se ti projekty v šuplíku? Dostaň je mezi lidi a získej cool tričko a body na profi IT kurzy v soutěži ITnetwork summer 2017!
Přidej si svou IT školu do profilu a najdi spolužáky zde na síti :)

2. díl - Testování v Javě - První unit test v JUnit

Java Testování Testování v Javě - První unit test v JUnit

ONEbit hosting Unicorn College Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V minulé lekci online kurzu o testování aplikací v Javě jsme si udělali poměrně solidní úvod do problematiky. Také jsme si uvedli v-model, který znázorňuje vztah mezi jednotlivými výstupy fází návrhu a příslušnými testy.

Testy tedy píšeme vždy na základě návrhu, nikoli implementace. Jinými slovy, děláme je na základě očekávané funkčnosti. Ta může být buď přímo od zákazníka (a to v případě akceptačních testů) nebo již od programátora (architekta), kde specifikuje jak se má která metoda chovat. Dnes se budeme věnovat právě těmto testům, kterým říkáme jednotkové (unit testy) a které testují detailní specifikaci aplikace, tedy její třídy.

Pamatujte, že nikdy nepíšeme testy podle toho, jak je něco uvnitř naprogramované! Velmi jednoduše by to mohlo naše myšlení svést jen tím daným způsobem a zapomněli bychom na to, že metodě mohou přijít třeba i jiné vstupy, na které není vůbec připravená. Testování s implementací ve skutečnosti vůbec nesouvisí, vždy testujeme zda je splněno zadání.

Jaké třídy testujeme

Unit testy testují jednotlivé metody ve třídách. Pro jistotu zopakuji, že nemá valný smysl testovat jednoúčelové metody např. v beanech nebo JavaFX aplikacích, které např. pouze něco vybírají z databáze. Abychom byli konkrétnější, nemá smysl testovat metodu jako je tato:

public void vlozPolozku(string nazev, double cena) {
        try (Connection spojeni = DriverManager.getConnection("jdbc:mysql://localhost/aplikace_db?user=root&password=");
                        PreparedStatement dotaz = spojeni.prepareStatement("INSERT INTO polozka (nazev, cena) VALUES (?, ?)");) {
                        dotaz.setString(1, nazev);
                        dotaz.setDouble(2, cena);
        } catch (SQLException ex) {
                        System.err.println("Chyba při komunikaci s databází");
        }
}

Metoda přidává položku do databáze. Typicky je použita jen v nějakém formuláři a pokud by nefungovala, zjistí to akceptační testy, jelikož by se nová položka neobjevila v seznamu. Podobných metod je v aplikaci hodně a zbytečně bychom ztráceli čas pokrýváním něčeho, co snadno pokryjeme v jiných testech.

Unit testy nalezneme nejčastěji u knihoven, tedy nástrojů, které programátor používá na více místech nebo dokonce ve více projektech a měly by být 100% funkční. Možná si vzpomenete, kdy jste použili nějakou knihovnu, staženou např. z GitHubu. Velmi pravděpodobně u ní byly také testy, které se nejčastěji vkládají do složky "test", která je vedle složky "src" v adresářové struktuře projektu. Pokud např. píšeme aplikaci, ve které často potřebujeme nějaké matematické výpočty, např. faktoriály a další pravděpodobnostní funkce, je samozřejmostí vytvořit si na tyto výpočty knihovnu a je velmi dobrý nápad pokrýt takovou knihovnu testy.

Příklad

Jak asi tušíte, my si podobnou třídu vytvoříme a zkusíme si ji otestovat. Abychom se nezdržovali, vytvořme si pouze jednoduchou kalkulačku, která bude umět:

  • sčítat
  • odčítat
  • násobit
  • dělit

Vytvoření projektu

V praxi by ve třídě byly nějaké složitější výpočty, ale tím se zde zabývat nebudeme. Vytvořte si nový projekt s názvem UnitTesty a do něj si přidejte třídu Kalkulacka a následující implementací:

package unittesty;

/**
 * Reprezentuje jednoduchou kalkulačku
 */
public class Kalkulacka {

    /**
     * Sečte 2 čísla
     * @param a První číslo
     * @param b Druhé číslo
     * @return Součet 2 čísel
     */
    public double secti(double a, double b) {
        return a + b;
    }

    /**
     * Odečte 2 čísla
     * @param a První číslo
     * @param b Druhé číslo
     * @return Rozdíl 2 čísel
     */
    public double odecti(double a, double b) {
        return a - b;
    }

    /**
     * Vynásobí 2 čísla
     * @param a První číslo
     * @param b Druhé číslo
     * @return Součin 2 čísel
     */
    public double vynasob(double a, double b) {
        return a * b;
    }

    /**
     * Vydělí 2 čísla
     * @param a První číslo
     * @param b Druhé číslo
     * @return Podíl 2 čísel
     */
    public double vydel(double a, double b) {
        if (b == 0)
            throw new IllegalArgumentException("Nelze dělit nulou!");
        return a / b;
    }

}

Na kódu je zajímavá pouze metoda vydel(), která vyvolá výjimku v případě, že dělíme nulou. Výchozí chování Javy u desetinných čísel je vrácení hodnoty "Infinity" (nekonečno), což v aplikaci není vždy to, co uživatel očekává.

Generování testů

V Javě pro testy používáme framework JUnit. Ten by měl být součástí NetBeans, pokud byste jej náhodou neměli nainstalovaný, lze jej jednoduše přidat v menu Tools -> Plugins. V levém panelu Projects klikneme na projekt pravým tlačítkem a zvolíme New -> Other.

Přidání unit testu v NetBeans v Javě

V následujícím dialogovém okně vybereme kategorii Unit Tests a typ souboru JUnit Test. Tím říkáme, že přidáváme nový unit test pro nějakou třídu.

Přidání JUnit testu v Javě

V posledním okně zadáme název testu, který se zpravidla sestavuje jako název testované třídy + slovo "Test", v našem případě tedy "KalkulackaTest". Testy obvykle přidáváme do balíčku "Test Packages".

Dokončení přidání unit testu v Javě

Potvrdíme a vygeneruje se nám nový soubor s následujícím kódem:

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;

public class KalkulackaTest {

    public KalkulackaTest() {
    }

    @BeforeClass
    public static void setUpClass() {
    }

    @AfterClass
    public static void tearDownClass() {
    }

    @Before
    public void setUp() {
    }

    @After
    public void tearDown() {
    }

    // TODO add test methods here.
    // The methods must be annotated with annotation @Test. For example:
    //
    // @Test
    // public void hello() {}
}

Asi vás v objektové Javě nepřekvapí, že je test třídy (scénář) reprezentovaný také třídou a jednotlivé testy metodami :) Co je již zajímavější je fakt, že na ni nalezneme několik předpřipravených metod, které jsou označené anotacemi. setUpClass() se zavolá jednou na začátku, před všemi testy. tearDownClass() funguje podobně, zavolá se jednou na konci, až všechny testy proběhnou.

Pokrytí třídy testy

Metody setUp() a tearDown(), přesněji metody s anotacemi @Before a @After, se zavolají před, resp. po každém testu v této třídě. To je pro nás velmi důležité, jelikož podle best practices chceme, aby byly testy nezávislé. Obvykle tedy před každým testem připravujeme znovu to samé prostředí, aby se vzájemně vůbec neovlivňovaly. O dobrých praktikách se zmímíne detailněji v příští lekci. Do třídy si přidejme atribut kalkulacka a v metodě setUp() v něm vždy vytvořme čerstvě novou kalkulačku pro každý test. Pokud by ji bylo ještě třeba dále nastavovat nebo bylo třeba vytvořit další závislosti, byly by také v této metodě:

public class KalkulackaTest {

    private Kalkulacka kalkulacka;

    public KalkulackaTest() {
    }

    @BeforeClass
    public static void setUpClass() {
    }

    @AfterClass
    public static void tearDownClass() {
    }

    @Before
    public void setUp() {
        // Nová kalkulačka je vytvořena před každým testem pro garantování jejich nezávislosti
        kalkulacka = new Kalkulacka();
    }

    ...

Pozn.: Jelikož jsou testy v jiném balíčku než třídy aplikace, naimportujte si třídu Kalkulacka kliknutím na ikonu vlevo u příslušného řádku.

Máme vše připraveno k přidávání samotných testů. Jednotlivé metody budou vždy označené anotací @Test a budou testovat jednu konkrétní metodu z třídy Kalkulacka, typicky pro několik různých vstupů. Pokud vás napadá proč metody označujeme anotacemi, umožňuje nám to vytvořit si i pomocné metody, které můžeme v daném testu využívat a které nebudou pokládány za testy. NetBeans nám totiž testy (metody s anotací @Test) automaticky spustí a vypíše jejich výsledky. Ve starších verzích JUnit musely místo anotací metody začínat na "test" a třída dědila ze scénáře (třídy TestCase).

Přidejme následujících 5 metod:

@Test
public void scitani() {
        assertEquals(2, kalkulacka.secti(1, 1), 0);
        assertEquals(1.42, kalkulacka.secti(3.14, -1.72), 0.001);
        assertEquals(2.0/3, kalkulacka.secti(1.0/3, 1.0/3), 0.001);
}

@Test
public void odcitani() {
        assertEquals(0, kalkulacka.odecti(1, 1), 0);
        assertEquals(4.86, kalkulacka.odecti(3.14, -1.72), 0.001);
        assertEquals(2.0/3, kalkulacka.odecti(1.0/3, -1.0/3), 0.001);
}

@Test
public void nasobeni() {
        assertEquals(2, kalkulacka.vynasob(1, 2), 0);
        assertEquals(-5.4008, kalkulacka.vynasob(3.14, -1.72), 0.001);
        assertEquals(0.111, kalkulacka.vynasob(1.0/3, 1.0/3), 0.001);
}

@Test
public void deleni() {
        assertEquals(2, kalkulacka.vydel(4, 2), 0);
        assertEquals(-1.826, kalkulacka.vydel(3.14, -1.72), 0.001);
        assertEquals(1, kalkulacka.vydel(1.0/3, 1.0/3), 0);
}

@Test(expected=IllegalArgumentException.class)
public void deleniVyjimka() {
        kalkulacka.vydel(2, 0);
}

K porovnávání výstupu metody s očekávanou hodnotou používáme metody assert*, staticky naimportované z balíčku org.junit.Assert. Nejčastěji asi použijete assertEquals(), která přijímá jako první parametr očekávanou hodnotu a jako druhý parametr hodnotu aktuální. Toto pořadí je dobré dodržovat, jinak budete mít hodnoty ve výsledcích testů opačně. Jak asi víte, desetinná čísla jsou v paměti počítače reprezentována binárně (jak jinak :) ) a to způsobí určitou ztrátu jejich přesnosti a také určité obtíže při jejich porovnávání. Proto musíme v tomto případě zadat i třetí parametr a to je delta, tedy kladná tolerance, o kolik se může očekávaná a aktuální hodnota lišit, aby test stále prošel. Všimněte si, že zkoušíme různé vstupy. Sčítání netestujeme jen jako 1 + 1 = 2, ale zkusíme celočíselné, desetinné i negativní vstupy, odděleně, a ověříme výsledky. V některých případech by nás mohla zajímat také maximální hodnota datových typů a podobně.

Poslední test ověřuje, zda metoda vydel() opravdu vyvolá výjimku při nulovém děliteli. Jak vidíte, nemusíme se zatěžovat s try-catch bloky, do anotace stačí pouze přidat parametr expected a uvést zde třídu výjimky, která se očekává. Pokud výjimka nenastane, test selže. Pro testování více případů vyvolání výjimky tímto způsobem by bylo třeba přidat více metod. K testování výjimek se ještě vrátíme příště.

Dostupné assert metody

Kromě metody assertEquals() můžeme použít ještě několik dalších, určitě se snažte použít tu nejvíce vyhovující metodu, zpřehledňuje to hlášky při selhání testů a samozřejmě i následnou opravu.

  • assertArrayEqu­als() - Zkontroluje, zda 2 pole obsahují ty samé prvky.
  • assertEquals() - Zkontroluje, zda jsou 2 hodnoty stejné (porovnává pomocí equals()).
  • assertFalse() - Zkontroluje, zda je hodnota false.
  • assertNotEquals() - Zkontroluje, zda 2 hodnoty nejsou stejné.
  • assertNotNull() - Zkontroluje, zda hodnota není null.
  • assertNotSame() - Zkontroluje, zda 2 reference neukazují na stejný objekt.
  • assertSame() - Zkontroluje, zda 2 reference ukazují na stejný objekt (porovnává pomocí ==).
  • assertTrue() - Zkontroluje, zda je hodnota true.

Metoda assertThat() umožňuje novější přístup k psaní assercí, ale tu si vysvětlíme až příště.

Spuštění testů

Testy spustíme kliknutím pravým tlačítkem na projekt a vybráním položky "Test" z kontextového menu.

Spuštění unit testů v Javě

NetBeans nám hezky vizuálně ukáže průběh testů (ty naše budou v okamžiku hotové) a také výsledky. Zobrazení i úspěšně proběhnutých testů zapnete pomocí ikony zelené fajfky.

Výsledek unit testů v NetBeans

Zkusme si nyní udělat v kalkulačce chybu, např. zakomentujme vyvolávání výjimky při dělení nulou:

public double vydel(double a, double b) {
        //if (b == 0)
        //    throw new IllegalArgumentException("Nelze dělit nulou!");
        return a / b;
}

A spusťme znovu naše testy:

Neúspěšný výsledek testů v NetBeans

Vidíme, že chyba je zachycena a jsme na ni upozorněni. Můžeme kód vrátit zpět do původního stavu. Příště se podíváme na změny v JUnit, uvedeme si knihovnu Hamcrest, naučíme se používat pravidla a zmíníme ty nejdůležitější best practices pro psaní testů.


 

Stáhnout

Staženo 4x (26.89 kB)
Aplikace je včetně zdrojových kódů v jazyce Java

 

 

Článek pro vás napsal David Čápka
Avatar
Jak se ti líbí článek?
2 hlasů
Autor pracuje jako softwarový architekt a pedagog na projektu ITnetwork.cz (a jeho zahraničních verzích). Velmi si váží svobody podnikání v naší zemi a věří, že když se člověk neštítí práce, tak dokáže úplně cokoli.
Unicorn College Autor se informační technologie naučil na Unicorn College - prestižní soukromé vysoké škole IT a ekonomie.
Aktivity (4)

 

 

Komentáře

Avatar
David.Landa
Člen
Avatar
David.Landa:14. dubna 20:40

Jestli autor takovým stylem píše někde ve firmě, tak to je mi té firmy líto :D

 
Odpovědět  -1 14. dubna 20:40
Avatar
Jan Vargovský
Redaktor
Avatar
Odpovídá na David.Landa
Jan Vargovský:14. dubna 23:03

Spíše si přečetl jeden tutorial o unit testech, napsal je v Javě a vzniklo z toho tohle.

Editováno 14. dubna 23:03
 
Odpovědět 14. dubna 23:03
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na David.Landa
David Čápka:14. dubna 23:39

A mohl bys prosím napsat co konkrétně se ti nezdá?

Odpovědět 14. dubna 23:39
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na Jan Vargovský
David Čápka:14. dubna 23:40

Napsal jsem jich několik set Honzo, ale dobrý pokus :) (těch testů)

Editováno 14. dubna 23:45
Odpovědět 14. dubna 23:40
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
Jan Vargovský
Redaktor
Avatar
Odpovídá na David Čápka
Jan Vargovský:15. dubna 12:29

A kolik jsi jich napsal v Javě? Já formuloval tu větu blbě... Četl jsem, že píšeš testy, ale ty píšeš v jiném jazyce, princip je sice stejný, ale existuje mnoho nástrojů a tutoriály ti bohužel léta zkušenosti nepředají :) Když porovnám testy v c#, tak máš x možností v čem to psát. S tím by ses měl dívat i na to, jak to integrovat do CI/CD. Nezmínil jsi tu ani AAA princip. Troufám si hádat, že v Javě to je dost podobné a mají taky x možností, proč jsi tedy zvolil zrovna tenhle unit test framework? Jaké existují alternativy?

 
Odpovědět 15. dubna 12:29
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na Jan Vargovský
David Čápka:15. dubna 12:59

Vaříš z vody, zkusil jsi to, nevyšlo to, nech to být. Unit testy jsou ve všech OOP jazycích stejné, navíc v Javě jsem je dělal na VŠ. O tom proč používám zrovna defaultní JUnit se vážně dohadovat nebudu, na to je venku moc hezky. AAA princip je o tom, že vytvoříš instanci, zavoláš metodu a uděláš assert, to v článku je. A že něco hádáš, no to je hezké, to je argument jak Brno.

Zkus se už přenést přes to, že dostávám zaplaceno za svoji práci, hejtoval jsi mě kvůli tomu přes PM a vždycky když se tu někomu něco nezdá, tak se přidáš a začneš vymýšlet umělé argumenty jak něco neumím, je to už trapné. (tvé další reakce jsem smazal, jelikož ti došly argumenty a začal jsi vytahovat že jsem někdy v minulosti nereagoval na tvůj požadavek, to už je vážně úplně mimo a jen to tu znečišťuje diskuzi :) ) Stejně tak jsem smazal i další reakci od Daniela, který místo odpovědi na to co se mu nezdá v článku zas napsal jak je celý projekt špatný bez jediného argumentu.

Editováno 16. dubna 13:21
Odpovědět  +2 15. dubna 12:59
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
Jakub Mareš
Člen
Avatar
Jakub Mareš:15. dubna 13:08

Mně se tento článek naopak líbil. Nebyl nijak složitý a myslím si, že například k AAA principu se po chvíli stejně dostane každý sám. A navíc jsme ho tu zmínili, a tak se o něm dozvěděli lidé alespoň tímto způsobem. Alternativy si podle mě může najít také každý sám. Chápu tento článek jako úvod do unit testů a jsem rád, že nebyl nijak přetěžující.

 
Odpovědět  +1 15. dubna 13:08
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 7 zpráv z 7.