Lekce 2 - Testování v Javě - První unit test v JUnit
V minulé lekci online kurzu o testování aplikací v Javě, Úvod do testování softwaru 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.
V dnešním tutoriálu kurzu Testování v Javě vytvoříme jednoduchou třídu v Javě, pro kterou vygenerujeme unit test pomocí frameworku JUnit. Ověříme tak funkčnost metod a vyvolávání výjimek.
Testy 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 (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.
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á valný 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 vzpomeneme, kdy jsme 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/
, oddělené od složky
s hlavními zdrojovými kódy, např. src/
nebo main/
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 - Kalkulačka
Jak asi tušíme, 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 IntelliJ IDEA si vytvoříme nový projekt s názvem
UnitTesty
:

V praxi by ve třídě byly nějaké složitější výpočty, ale tím se
zde zabývat nebudeme. Do vytvořeného projektu si přidejme třídu
Kalkulacka
s následující implementací:
package cz.itnetwork; /** * 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 musíme v IntelliJ IDEA přidat mezi závislosti v menu File -> Project Structure. Pod možností Project Settings vybereme Libraries, klikneme na + a vybereme From Maven:

V následujícím dialogovém okně specifikujeme potřebnou knihovnu i s
verzí org.junit.jupiter:junit-jupiter:5.10.2
a potvrdíme
kliknutím na OK:

Potvrdíme, že chceme přidat závislost do modulu UnitTesty
,
aplikujeme změny kliknutím na Apply a potvrdíme OK. Než
vytvoříme náš první test, vytvoříme si složku test/
. V
levém panelu Project klikneme na UnitTesty
pravým
tlačítkem a zvolíme New -> Directory, vložíme název
test
a potvrdíme.
Vrátíme se do třídy Kalkulacka
. Klikneme na deklaraci
třídy Kalkulacka
a stiskneme klávesovou zkratku Alt +
Enter. Vybereme možnost Create Test:

V následujícím dialogovém okně vidíme výchozí název testu, který se
zpravidla sestavuje jako název testované třídy a slovo
Test, v našem případě tedy KalkulackaTest
.
Výchozí balíček pro vygenerovanou třídu se jmenuje stejně jako balíček
původní třídy. Zaklikneme možnosti setUp/@Before a
tearDown/@After:

Potvrdíme a vygeneruje se nám nový soubor s následujícím kódem:
package cz.itnetwork; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import static org.junit.jupiter.api.Assertions.*; class KalkulackaTest { @BeforeEach public void setUp() { } @AfterEach public void tearDown() { } }
Asi nás v objektové Javě nepřekvapí, že je test třídy (scénář)
reprezentovaný také třídou a jednotlivé testy
metodami
Zajímavější je fakt, že v ní nalezneme několik předpřipravených metod,
které jsou označené anotacemi. Metody
setUp()
a
tearDown()
, přesněji metody s anotacemi @BeforeEach
a @AfterEach
, 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 na sobě
navzájem nezávislé. Obvykle tedy před každým testem
připravujeme znovu to samé prostředí, aby se jednotlivé testy vzájemně
vůbec neovlivňovaly. O dobrých praktikách se zmíníne detailněji v lekci
Testování
v Javě - Hamcrest, JUnit TestRule a best practices.
Pokrytí třídy testy
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; @BeforeEach public void setUp() { // Nová kalkulačka je vytvořena před každým testem pro garantování jejich nezávislosti kalkulacka = new Kalkulacka(); } ...
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 obvykle testovat
jednu konkrétní metodu z třídy Kalkulacka
,
typicky pro několik různých vstupů. Pokud ná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. IntelliJ IDEA 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í názvy metod začínat slovem
"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 public void deleniVyjimka() { assertThrows(IllegalArgumentException.class, () -> 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.jupiter.api.Assertions.*
.
Nejčastěji použijeme 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ě.
Desetinná čísla bychom nikdy neměli srovnávat na přesnou shodu.
Jak asi víme, 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ěme 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íme, nemusíme se zatěžovat
s try-catch bloky, stačí použít metodu assertThrows()
,
která jako první parametr přijímá třídu výjimky, která
se očekává. Jako druhý parametr přijímá funkční rozhraní
Executable
, kde můžeme zavolat kód, který testujeme
pomocí lambda výrazu. Je to proto, že metoda musí dostat kód,
který teprve bude spouštět (aby ověřila, zda dojde k vyvolání výjimky).
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 v lekci Testování
v Javě - Hamcrest, JUnit TestRule a best practices.
Dostupné assert*
metody
Kromě metody assertEquals()
můžeme použít ještě několik
dalších, určitě se snažme 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.
assertArrayEquals()
- Zkontroluje, zda 2 pole obsahují ty samé prvky.assertEquals()
- Zkontroluje, zda jsou 2 hodnoty stejné (porovnává pomocíequals()
).assertNotEquals()
- Zkontroluje, zda 2 hodnoty nejsou stejné.assertNotSame()
- Zkontroluje, zda 2 reference neukazují na stejný objekt.assertSame()
- Zkontroluje, zda 2 reference ukazují na stejný objekt (porovnává pomocí==
).assertNotNull()
- Zkontroluje, zda hodnota není null.assertNull()
- Zkontroluje, zda je hodnota null.assertFalse()
- Zkontroluje, zda je hodnota false.assertTrue()
- Zkontroluje, zda je hodnota true.assertDoesNotThrow()
- Zkontroluje, zda se nevyvolá výjimka.
Spuštění testů
Testy lze spustit kliknutím na Run KalkulackaTest po kliknutí pravým tlačítkem na KalkulackaTest v levém panelu:

IntelliJ IDEA nám hezky vizuálně ukáže průběh testů (ty naše budou v okamžiku hotové) a také výsledky:

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:

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.
V následujícím kvízu, Kvíz - Úvod do testování a unit testů v Javě, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 144x (6.04 kB)
Aplikace je včetně zdrojových kódů v jazyce Java