2. diel - Testovanie v Jave - Prvý unit test v JUnit
V minulej lekcii online kurzu o testovaní aplikácií v Jave, Úvod do testovania softvéru 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áz návrhu a príslušnými testami.
V dnešnom tutoriále kurzu Testovania v Jave vytvoríme jednoduchú triedu v Jave, pre ktorú vygenerujeme unit test pomocou frameworku JUnit. Overíme tak funkčnosť metód a vyvolávanie výnimiek.
Testy píšeme vždy na základe návrhu, nie implementácie. Inými slovami, robíme ich na základe očakávanej funkčnosti. Tá môže byť buď priamo od zákazníka (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.
Nikdy nepíšeme testy podľa toho, ako je niečo vo 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 beanoch alebo JavaFX aplikáciách, ktoré napr. iba niečo vyberajú z databázy. Aby sme boli konkrétnejší, nemá veľký zmysel testovať metódu ako je táto:
public void insertItem(String name, double price) { try (Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/app_db?user=root&password="); PreparedStatement statement = connection.prepareStatement("INSERT INTO item (name, price) VALUES (?, ?)");) { statement.setString(1, name); statement.setDouble(2, price); } catch (SQLException ex) { System.err.println("Error while communicating with the database"); } }
Metóda pridáva položku do databázy. Typicky je použitá len v nejakom formulári a pokiaľ 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 pri
knižniciach, teda nástrojoch, 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 spomenieme, kedy sme použili nejakú
knižnicu, stiahnutú napr. z GitHubu. Veľmi pravdepodobne u nej boli aj testy,
ktoré sa najčastejšie vkladajú do zložky test/
, oddelené od
zložky s hlavnými zdrojovými kódmi, napr. src/
alebo
main/
v adresárovej štruktúre projektu. Pokiaľ napr. píšeme
aplikáciu, v ktorej často potrebujeme nejaké matematické výpočty, napr.
faktoriály 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úto
knižnicu testami.
Príklad - Kalkulačka
Ako asi tušíme, my si podobnú triedu vytvoríme a skúsime si ju otestovať. Aby sme sa nezdržiavali, vytvorme si iba jednoduchú kalkulačku, ktorá bude vedieť:
- sčítať,
- odčítať,
- násobiť,
- deliť.
Vytvorenie projektu
V IntelliJ IDEA si vytvoríme nový projekt s názvom
UnitTests
:
V praxi by v triede boli nejaké zložitejšie výpočty, ale tým sa tu
zaoberať nebudeme. Do vytvoreného projektu si pridajme triedu
Calculator
s nasledujúcou implementáciou:
package com.ictdemy; /** * Represents a simple calculator */ public class Calculator { /** * Sums 2 numbers * @param a The first number * @param b The second number * @return The sum of 2 numbers */ public double add(double a, double b) { return a + b; } /** * Subtracts 2 numbers * @param a The first number * @param b The second number * @return The difference of 2 numbers */ public double subtract(double a, double b) { return a - b; } /** * Multiplies 2 numbers * @param a The first number * @param b The second number * @return The product of 2 numbers */ public double multiply(double a, double b) { return a * b; } /** * Divides 2 numbers * @param a The first number * @param b The second number * @return The quotient of 2 numbers */ public double divide(double a, double b) { if (b == 0) throw new IllegalArgumentException("Cannot divide by zero!"); return a / b; } }
Na kóde je zaujímavá iba metóda divide()
, 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 musíme v IntelliJ IDEA pridať medzi závislosti v menu File -> Project Structure. Pod možnosťou Project Settings vyberieme Libraries, klikneme na + a vyberieme From Maven:
V nasledujúcom dialógovom okne špecifikujeme potrebnú knižnicu aj s
verziou org.junit.jupiter:junit-jupiter:5.10.2
a potvrdíme
kliknutím na OK:
Potvrdíme, že chceme pridať závislosť do modulu UnitTests
,
aplikujeme zmeny kliknutím na Apply a potvrdíme OK. Než
vytvoríme náš prvý test, vytvoríme si zložku test/
. V ľavom
paneli Project klikneme na UnitTests
pravým tlačidlom a
zvolíme New -> Directory, vložíme názov
test
a potvrdíme.
Vrátime sa do triedy Calculator
. Klikneme na deklaráciu triedy
Calculator
a stlačíme klávesovú skratku Alt +
Enter. Vyberieme možnosť Create Test:
V nasledujúcom dialógovom okne vidíme východiskový názov testu, ktorý
sa spravidla zostavuje ako názov testovanej triedy a slovo
Test, v našom prípade teda CalculatorTest
.
Predvolený balíček pre vygenerovanú triedu sa volá rovnako ako balíček
pôvodnej triedy. Zaklikneme možnosti setUp/@Before a
tearDown/@After:
Potvrdíme a vygeneruje sa nám nový súbor s nasledujúcim kódom:
package com.ictdemy; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import static org.junit.jupiter.api.Assertions.*; class CalculatorTest { @BeforeEach void setUp() { } @AfterEach void tearDown() { } }
Asi nás v objektovej Jave neprekvapí, že je test triedy (scenár)
reprezentovaný aj triedou a jednotlivé testy
metódami
Zaujímavejší je fakt, že v nej nájdeme niekoľko predpripravených metód,
ktoré sú označené anotáciami. Metódy setUp()
a
tearDown()
, presnejšie metódy s anotáciami
@BeforeEach
a @AfterEach
, 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 na sebe
navzájom nezávislé. Obvykle teda pred každým testom
pripravujeme znova to isté prostredie, aby sa jednotlivé testy vzájomne
vôbec neovplyvňovali. O dobrých praktikách sa zmienime detailnejšie v
lekcii Testovanie
v Jave - Hamcrest, JUnit TestRule a best practices.
Pokrytie triedy testy
Do triedy si pridajme atribút calculator
a v metóde
setUp()
v ňom vždy vytvorme čerstvo novú kalkulačku pre
každý test. Ak by ju bolo ešte potrebné ďalej nastavovať alebo bolo treba
vytvoriť ďalšie závislosti, boli by rovnako v tejto metóde:
public class CalculatorTest{ private Calculator calculator; @BeforeEach void setUp() { // A new calculator is always created before each test to guarantee their independence calculator = new Calculator(); } ...
Máme všetko pripravené na pridávanie samotných testov. Jednotlivé
metódy budú vždy označené anotáciou @Test
a budú obvykle
testovať jednu konkrétnu metódu z triedy
Calculator
, typicky pre niekoľko rôznych
vstupov. Pokiaľ ná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ú pokladané za testy.
IntelliJ IDEA 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 namiesto anotácií názvy metód začínať slovom "test" a trieda
dedila zo scenára (triedy TestCase
).
Pridajme nasledujúcich 5 metód:
@Test public void add() { assertEquals(2, calculator.add(1, 1), 0); assertEquals(1.42, calculator.add(3.14, -1.72), 0.001); assertEquals(2.0/3, calculator.add(1.0/3, 1.0/3), 0.001); } @Test public void subtract() { assertEquals(0, calculator.subtract(1, 1), 0); assertEquals(4.86, calculator.subtract(3.14, -1.72), 0.001); assertEquals(2.0/3, calculator.subtract(1.0/3, -1.0/3), 0.001); } @Test public void multiply() { assertEquals(2, calculator.multiply(1, 2), 0); assertEquals(-5.4008, calculator.multiply(3.14, -1.72), 0.001); assertEquals(0.111, calculator.multiply(1.0/3, 1.0/3), 0.001); } @Test public void divide() { assertEquals(2, calculator.divide(4, 2), 0); assertEquals(-1.826, calculator.divide(3.14, -1.72), 0.001); assertEquals(1, calculator.divide(1.0/3, 1.0/3), 0); } @Test public void divideException() { assertThrows(IllegalArgumentException.class, () -> calculator.divide(2, 0)); }
Na porovnávanie výstupu metódy s očakávanou
hodnotou používame metódy assert*()
, staticky
naimportované z balíčka org.junit.jupiter.api.Assertions.*
.
Najčastejšie použijeme assertEquals()
, ktorá prijíma ako prvý
parameter očakávanú hodnotu a ako druhý parameter hodnotu aktuálnu. Toto
poradie je dobré dodržiavať, inak budete mať hodnoty vo výsledkoch testov
opačne.
Desatinné čísla by sme nikdy nemali porovnávať na presnú zhodu.
Ako asi vieme, 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šimnime 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ť aj maximálna hodnota dátových typov a podobne.
Posledný test overuje, či metóda divide()
naozaj vyvolá
výnimku pri nulovom deliteľovi. Ako vidíme, nemusíme sa zaťažovať
s try-catch blokmi, stačí použiť metódu
assertThrows()
, ktorá ako prvý parameter prijíma triedu
výnimky, ktorá sa očakáva. Ako druhý parameter prijíma funkčné
rozhranie Executable
, kde môžeme zavolať kód, ktorý
testujeme pomocou lambda výrazu. Je to preto, že metóda musí
dostať kód, ktorý ešte len bude spúšťať (aby overila, či dôjde k
vyvolaniu výnimky). Ak výnimka nenastane, test zlyhá. Na
testovanie viacerých prípadov vyvolania výnimky týmto spôsobom by bolo
potrebné pridať viac metód. K testovaniu výnimiek sa ešte vrátime v lekcii
Testovanie
v Jave - Hamcrest, JUnit TestRule a best practices.
Dostupné assert*
metódy
Okrem metódy assertEquals()
môžeme použiť ešte niekoľko
ďalších, určite sa snažme použiť tú najviac vyhovujúcu 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 pomocouequals()
).assertNotEquals()
- Skontroluje, či 2 hodnoty nie sú rovnaké.assertNotSame()
- Skontroluje, či 2 referencie neukazujú na rovnaký objekt.assertSame()
- Skontroluje, či 2 referencie ukazujú na rovnaký objekt (porovnáva pomocou==
).assertNotNull()
- Skontroluje, či hodnota nie je null.assertNull()
- Skontroluje, či je hodnota null.assertFalse()
- Skontroluje, či je hodnota false.assertTrue()
- Skontroluje, či je hodnota true.assertDoesNotThrow()
- Skontroluje, či sa nevyvolá výnimka.
Spustenie testov
Testy je možné spustiť kliknutím na Run CalculatorTest po kliknutí pravým tlačidlom na CalculatorTest v ľavom paneli:
IntelliJ IDEA nám pekne vizuálne ukáže priebeh testov (tie naše budú v okamihu hotové) a taktiež výsledky:
Skúsme si teraz urobiť v kalkulačke chybu, napr. zakomentujme vyvolávanie výnimky pri delení nulou:
public double divide(double a, double b) { // if (b == 0) // throw new IllegalArgumentException("Cannot divide by zero!"); 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.
V nasledujúcom kvíze, Kvíz - Úvod do testovania a unit testov v Jave, si vyskúšame nadobudnuté skúsenosti z predchádzajúcich lekcií.
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é 7x (9.88 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java