2. diel - Testovanie v Jave - Prvý unit test v JUnit
V minulej lekcii online kurzu o testovanie aplikácií v Jave sme si urobili pomerne solídny úvod do problematiky. Tiež sme si uviedli v-model, ktorý znázorňuje vzťah medzi jednotlivými výstupmi fázou návrhu a príslušnými testami.
Testy teda píšeme vždy na základe návrhu, nie implementácie. Inými slovami, robíme je na základe očakávanej funkčnosti. Tá môže byť buď priamo od zákazníka (a to v prípade akceptačných testov) alebo už od programátora (architekta), kde špecifikuje ako sa má ktorá metóda správať. Dnes sa budeme venovať práve týmto testom, ktorým hovoríme jednotkové (unit testy) a ktoré testujú detailnú špecifikáciu aplikácie, teda jej triedy.
Pamätajte, že nikdy nepíšeme testy podľa toho, ako je niečo vnútri naprogramované! Veľmi jednoducho by to mohlo naše myslenie zviesť len tým daným spôsobom a zabudli by sme na to, že metóde môžu prísť napríklad aj iné vstupy, na ktoré nie je vôbec pripravená. Testovanie s implementáciou v skutočnosti vôbec nesúvisí, vždy testujeme či je splnené zadanie.
Aké triedy testujeme
Unit testy testujú jednotlivé metódy v triedach. Pre istotu zopakujem, že nemá veľký zmysel testovať jednoúčelové metódy napr. V bean alebo JavaFX aplikáciách, ktoré napr. Len niečo vyberajú z databázy. Aby sme boli konkrétnejší, nemá zmysel testovať metódu ako je táto:
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í"); } }
Metóda pridáva položku do databázy. Typicky je použitá len v nejakom formulári a ak by nefungovala, zistí to akceptačné testy, pretože by sa nová položka neobjavila v zozname. Podobných metód je v aplikácii veľa a zbytočne by sme strácali čas pokrývaním niečoho, čo ľahko pokryjeme v iných testoch.
Unit testy nájdeme najčastejšie u knižníc, teda nástrojov, ktoré programátor používa na viacerých miestach alebo dokonca vo viacerých projektoch a mali by byť 100% funkčný. Možno si spomeniete, kedy ste použili nejakú knižnicu, stiahnutú napr. Z GitHub. Veľmi pravdepodobne u nej boli tiež testy, ktoré sa najčastejšie vkladajú do zložky "test", ktorá je vedľa zložky "src" v adresárovej štruktúre projektu. Ak napr. Píšeme aplikáciu, v ktorej často potrebujeme nejaké matematické výpočty, napr. Faktoriál a ďalšie pravdepodobnostné funkcie, je samozrejmosťou vytvoriť si na tieto výpočty knižnicu a je veľmi dobrý nápad pokryť takú knižnicu testy.
Príklad
Ako asi tušíte, my si podobnú triedu vytvoríme a skúsime si ju otestovať. Aby sme sa nezdržovali, vytvorme si iba jednoduchú kalkulačku, ktorá bude vedieť:
- sčítať
- odčítať
- násobiť
- deliť
Vytvorenie projektu
V praxi by v triede boli nejaké zložitejšie výpočty, ale tým sa tu zaoberať nebudeme. Vytvorte si nový projekt s názvom UnitTesty a do neho si pridajte triedu Kalkulacka a nasledujúce implementácií:
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 zaujímavá iba metóda vydel()
, ktorá vyvolá
výnimku v prípade, že delíme nulou. Predvolené správanie Javy u
desatinných čísel je vrátenie hodnoty "Infinity" (nekonečno), čo v
aplikácii nie je vždy to, čo používateľ očakáva.
Generovanie testov
V Jave pre testy používame framework JUnit. Ten by mal byť súčasťou NetBeans, ak by ste ho náhodou nemali nainštalovaný, možno ho jednoducho pridať v menu Tools -> Plugins. V ľavom paneli Projects klikneme na projekt pravým tlačidlom a zvolíme New -> Other.
V nasledujúcom dialógovom okne vyberieme kategórii Unit Tests a typ súboru JUnit Test. Tým hovoríme, že pridávame nový unit test pre nejakú triedu.
V poslednom okne zadáme názov testu, ktorý sa spravidla zostavuje ako názov testované triedy + slovo "Test", v našom prípade teda "KalkulackaTest". Testy zvyčajne pridávame do balíčka "Test Packages".
Potvrdíme a vygeneruje sa nám nový súbor s nasledujúcim kódom:
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 objektovej Jave neprekvapí, že je test triedy (scenár)
reprezentovaný tiež triedou a jednotlivé testy metódami Čo je už zaujímavejšie je fakt,
že na ňu nájdeme niekoľko predpripravených metód, ktoré sú označené
anotáciami. setUpClass()
sa zavolá raz na začiatku, pred
všetkými testami. tearDownClass()
funguje podobne, zavolá sa raz
na konci, až všetky testy prebehnú.
Pokrytie triedy testy
Metódy setUp()
a tearDown()
, presnejšie metódy s
anotáciami @Before
a @After
, sa zavolajú pred, resp.
po každom teste v tejto triede. To je pre nás veľmi
dôležité, pretože podľa best practices chceme, aby boli testy
nezávislé. Obvykle teda pred každým testom pripravujeme
znovu to isté prostredie, aby sa vzájomne vôbec neovplyvňovali. O dobrých
praktikách sa zmímíne detailnejšie v nasledujúcej lekcii. Do triedy si
pridajme atribút kalkulacka
a v metóde setUp()
v
ňom vždy vytvorme čerstvo novú kalkulačku pre každý test. Pokiaľ by ju
bolo ešte potrebné ďalej nastavovať alebo bolo treba vytvoriť ďalšie
závislosti, boli by tiež v tejto metóde:
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 .: Keďže sú testy v inom balíčku než triedy aplikácie,
importujte si triedu Kalkulacka
kliknutím na ikonu vľavo pri
príslušnom riadku.
Máme všetko pripravené na pridávanie samotných testov. Jednotlivé
metódy budú vždy označené anotácií @Test
a budú testovať
jednu konkrétnu metódu z triedy Kalkulacka
, typicky pre niekoľko
rôznych vstupov. Ak vás napadá prečo metódy označujeme anotáciami,
umožňuje nám to vytvoriť si aj pomocné metódy, ktoré môžeme v danom
teste využívať a ktoré nebudú považované za testy. NetBeans nám totiž
testy (metódy s anotáciou @Test
) automaticky spustí a vypíše
ich výsledky. V starších verziách JUnit museli miesto anotácií metódy
začínať na "test" a trieda dedila zo scenára (triedy
TestCase
).
Pridajme nasledujúcich 5 metód:
@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ávanie výstupu metódy s očakávanou hodnotou používame metódy
Assert *, staticky importované z balíčka org.junit.Assert
.
Najčastejšie asi použijete assertEquals()
, ktorá prijíma ako
prvý parameter očakávanú hodnotu a ako druhý parameter hodnotu aktuálny.
Toto poradie je dobré dodržiavať, inak budete mať hodnoty vo výsledkoch
testov opačne. Ako asi viete, desatinné čísla sú v pamäti počítača
reprezentovaná binárne (ako inak ) A to spôsobí určitú stratu ich presnosti a tiež určité
ťažkosti pri ich porovnávaní. Preto musíme v tomto prípade zadať aj
tretí parameter a to je delta, teda kladná tolerancia, o
koľko sa môže očakávaná a aktuálna hodnota líšiť, aby test stále
prešiel. Všimnite si, že skúšame rôzne vstupy. Sčítanie netestujeme len
ako 1 + 1 = 2, ale skúsime celočíselné, desatinné aj negatívne vstupy,
oddelene, a overíme výsledky. V niektorých prípadoch by nás mohla
zaujímať tiež maximálna hodnota dátových typov a podobne.
Posledný test overuje, či metóda vydel()
naozaj vyvolá
výnimku pri nulovom deliteľmi. Ako vidíte, nemusíme sa zaťažovať s
try-catch bloky, do anotácie stačí iba pridať parameter expected a uviesť
tu triedu výnimky, ktorá sa očakáva. Ak výnimka nenastane, test zlyhá. Pre
testovanie viac prípadov vyvolanie výnimky týmto spôsobom by bolo treba
pridať viac metód. K testovaniu výnimiek sa ešte vrátime nabudúce.
Dostupné Assert metódy
Okrem metódy assertEquals()
môžeme použiť ešte niekoľko
ďalších, určite sa snažte použiť tú najviac vyhovujúce metódu,
sprehľadňuje to hlášky pri zlyhaní testov a samozrejme aj následnú
opravu.
- assertArrayEquals () - Skontroluje, či 2 polia obsahujú tie isté prvky.
- assertEquals () - Skontroluje, či sú 2 hodnoty rovnaké
(porovnáva pomocou
equals()
). - assertFalse () - Skontroluje, či je hodnota false.
- assertNotEquals () - Skontroluje, či 2 hodnoty nie sú rovnaké.
- assertNotNull () - Skontroluje, či hodnota nie je null.
- assertNotSame () - Skontroluje, či 2 referencie neukazujú na rovnaký objekt.
- assertSame () - Skontroluje, či 2 referencie ukazujú na rovnaký objekt (porovnáva pomocou ==).
- assertTrue () - Skontroluje, či je hodnota true.
Metóda assertThat()
umožňuje novšie prístup k písanie
assercí, ale tú si vysvetlíme až nabudúce.
Spustenie testov
Testy spustíme kliknutím pravým tlačidlom na projekt a vybraním položky "Test" z kontextového menu.
NetBeans nám pekne vizuálne ukáže priebeh testov (tie naše budú v okamihu hotové) a tiež výsledky. Zobrazenie aj úspešne prebehnutých testov zapnete pomocou ikony zelenej fajky.
Skúsme si teraz urobiť v kalkulačke chybu, napr. Zakomentujte vyvolávanie výnimky pri delení nulou:
public double vydel(double a, double b) { //if (b == 0) // throw new IllegalArgumentException("Nelze dělit nulou!"); return a / b; }
A spustite znovu naše testy:
Vidíme, že chyba je zachytená a sme na ňu upozornení. Môžeme kód vrátiť späť do pôvodného stavu. Nabudúce sa pozrieme na zmeny v JUnit, uvedieme si knižnicu Hamcrest, naučíme sa používať pravidlá a zmienime tie najdôležitejšie best practices pre písanie testov.
Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.
Stiahnuť
Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami
Stiahnuté 110x (26.89 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java