2. diel - Testovanie v Kotline - Prvý unit test pomocou JUnit
V minulej lekcii online kurzu o testovaní aplikácií v Kotline, Úvod do testovania softvéru v Kotlin , sme si uviedli základné typy testov a 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 Kotline tutoriále venovanom testovaní si vytvoríme jednoduchú kalkulačku a pokryjeme ju unit testy. Zopakujeme si, čo jednotkovými (unit) testami testujeme a ukážeme si, ako ich do našej aplikácie pridať.
Použitie unit testov
Vieme, že testy píšeme vždy na základe návrhu, nie implementácie. Kontrolujeme nimi teda očakávanú funkčnosť. Pokiaľ takú funkčnosť definuje zákazník, siahneme po akceptačných testoch. Môže byť tiež definovaná programátorom, ktorý špecifikuje, ako sa má daná metóda správať. V druhom prípade použijeme unit testy, ktoré testujú detailnú špecifikáciu aplikácie, teda jej triedy.
Pamätajme, že 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ť. Napríklad by sme zabudli, že metóde môžu prísť 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 netestujeme jednoúčelové metódy, ktoré napr. vo formulárových aplikáciách iba niečo vyberajú z databázy. Aby sme boli konkrétnejší, nemá veľký zmysel testovať metódu ako je táto:
fun vlozPolozku(nazev: String, cena: Double) { try { val spojeni = DriverManager.getConnection("jdbc:mysql://localhost/aplikace_db?user=root&password=") val dotaz = spojeni.prepareStatement("INSERT INTO polozka(nazev, cena) VALUES(?, ?") dotaz.setString(1, nazev) dotaz.setDouble(2, cena) } catch (ex: SQLException) { System.err.println("Chyba pri komunikácii s databázou") } }
Metóda pridáva položku do databázy. Typicky je použitá len v nejakom formulári. 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 pri knižniciach, teda
nástrojoch, ktoré programátor používa na viacerých
miestach alebo dokonca vo viacerých projektoch. Pokiaľ používame
nejakú kvalitnú knižnicu, stiahnutú napr. z GitHubu, nájdeme u nej aj
testy, ktoré sa najčastejšie vkladajú do zložky src/test/
v
adresárovej štruktúre projektu.
Ak 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.
Jednoduchá kalkulačka s testami
My si teraz podobnú triedu vytvoríme a skúsime ju otestovať. Bude to iba jednoduchá kalkulačka, ktorá bude vedieť:
- sčítať,
- odčítať,
- násobiť,
- deliť.
V praxi by v triede boli nejaké zložitejšie výpočty, ale tým sa tu
zaoberať nebudeme. Nový projekt nazveme UnitTesty
a pridáme doň
triedu Kalkulacka
s nasledujúcou implementáciou:
/** * * Reprezentuje jednoduchou kalkulačku */ class Kalkulacka { /** * Sečte 2 čísla * @param a První číslo * @param b Druhé číslo * @return Součet 2 čísel */ fun secti(a: Double, b: Double): Double { return a + b } /** * Odečte 2 čísla * @param a První číslo * @param b Druhé číslo * @return Rozdíl 2 čísel */ fun odecti(a: Double, b: Double): Double { return a - b } /** * Vynásobí 2 čísla * @param a První číslo * @param b Druhé číslo * @return Součin 2 čísel */ fun vynasob(a: Double, b: Double): Double { return a * b } /** * Vydělí 2 čísla * @param a První číslo * @param b Druhé číslo * @return Podíl 2 čísel */ fun vydel(a: Double, b: Double): Double { if (b == 0.0) throw IllegalArgumentException("Nemožno deliť nulou!") return a / b } }
Na kóde je zaujímavá iba metóda vydel()
, ktorá vyvolá
výnimku v prípade, že delíme nulou.
Predvolené správanie pri desatinných čísel v Kotline je
vrátenie hodnoty Infinity
(nekonečno), čo v aplikácii nie je
vždy to, čo používateľ očakáva.
Generovanie testov
Na testy používame Java framework JUnit, ktorý je súčasťou IntelliJ. Pravým tlačidlom klikneme na názov triedy a zvolíme Generate:
V nasledujúcom dialógovom okne vyberieme položku Test:
V poslednom okne zadáme názov testu. Konvenčne sa za názov testovanej
triedy pripojí slovo Test
, v našom prípade vytvoríme triedu
KalkulackaTest
. Začiarkneme tiež možnosti setUp a
tearDown na vygenerovanie potrebných metód. Ich význam si
vysvetlíme neskôr:
Potvrdíme a vygeneruje sa nám nový súbor s nasledujúcim kódom:
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test public class KalkulackaTest { @BeforeEach fun setUp() { } @AfterEach fun tearDown() { } }
Vidíme, že celý test je trieda a jednotlivé testy reprezentujú metódy. Dokonca tu nájdeme dve predpripravené metódy, ktoré sú označené anotáciami.
Metódy setUp()
a
tearDown()
Metódy setUp()
a tearDown()
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 nezávislé. Obvykle teda pred každým testom
pripravujeme znova to isté prostredie, aby sa vzájomne vôbec
neovplyvňovali.
Do triedy si pridajme premennú kalkulacka
av metóde
setUp()
do nej vytvoríme vždy novú inštanciu pre každý test.
Ak by ju bolo ešte potrebné ďalej nastavovať alebo vytvoriť ďalšie
závislosti, boli by tiež v tejto metóde:
class KalkulackaTest { private lateinit var kalkulacka: Kalkulacka @BeforeEach fun setUp() { kalkulacka = Kalkulacka() } @AfterEach fun tearDown() { } }
Pokrytie triedy testy
Máme všetko pripravené na pridanie samotných testov. Jednotlivé metódy
budú vždy označené anotáciou @Test
a budú testovať jednu
konkrétnu metódu z triedy Kalkulacka
, typicky pre niekoľko
rôznych vstupov.
Metódy označujeme anotáciami, aby sme mali možnosť vytvoriť si aj
pomocné metódy, ktoré budeme v danom teste využívať a ktoré nebudú
pokladané za testy. IntelliJ nám totiž metódy s anotáciou
@Test
automaticky spustí a vypíše ich výsledky.
V starších verziách JUnit museli namiesto anotácií metódy
začínať textom test
a trieda dedila z triedy
TestCase
.
Pridajme nasledujúcich päť metód:
@Test fun scitani() { assertEquals(2.0, kalkulacka.secti(1.0, 1.0), 0.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 fun odcitani() { assertEquals(0.0, kalkulacka.odecti(1.0, 1.0), 0.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 fun nasobeni() { assertEquals(2.0, kalkulacka.vynasob(1.0, 2.0), 0.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 fun deleni() { assertEquals(2.0, kalkulacka.vydel(4.0, 2.0), 0.0) assertEquals(-1.826, kalkulacka.vydel(3.14, -1.72), 0.001) assertEquals(1.0, kalkulacka.vydel(1.0/3, 1.0/3), 0.0) } @Test fun deleniVyjimka() { assertThrows(IllegalArgumentException::class.java) { kalkulacka.vydel(2.0, 0.0) } }
Na porovnávanie výstupu metódy s očakávanou hodnotou používame
assert
metódy, staticky naimportované z balíčka
org.junit.jupiter.api.Assertions
. Najčastejšie asi použijeme
assertEquals()
, ktorá prijíma ako prvý parameter
očakávanú hodnotu a ako druhý parameter hodnotu
aktuálnu.
Pretože sú desatinné čísla v pamäti počítača reprezentované binárne, spôsobí to určitú stratu ich presnosti a tiež určité ťažkosti pri ich porovnávaní. V tomto prípade musíme teda zadať aj tretí parameter a to je delta, teda kladná tolerancia. Určíme tak, o koľko sa môže očakávaná a aktuálna hodnota líšiť, aby test stále prešiel.
Dôležité je tiež vyskúšať rôzne vstupy. Sčítanie netestujeme len ako 1 + 1 = 2, ale skúsime celočíselné, desatinné aj negatívne vstupy. 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 vydel()
naozaj vyvolá
výnimku pri nulovom deliteľovi. Nemusíme sa zaťažovať s
try-catch
blokmi, stačí danú časť kódu iba obaliť metódou
assertThrows()
a uviesť tu triedu 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. To si ukážeme v niektorej z ďalších lekcií.
Dostupné assert
metódy
Okrem metódy assertEquals()
môžeme použiť niekoľko
podobných:
- Metóda
assertArrayEquals()
skontroluje, či dve polia obsahujú tie isté prvky. - Metódami
assertTrue()
aassertFalse()
kontrolujeme, či je vrátená hodnotatrue
, resp.false
. - Pokiaľ očakávame rozdielne hodnoty, použijeme metódu
assertNotEquals()
. - Metóda
assertNotNull()
kontroluje hodnotunull
. - Na porovnanie referencií na objekt využijeme metódu
assertSame()
alebo jej negáciuassertNotSame()
.
assertThat()
, ktorá umožňuje novší
prístup k písaniu assercií, ale tú si vysvetlíme až nabudúce.
Spustenie testov
Testy teraz spustíme kliknutím pravým tlačidlom na projekt a vybraním položky Run 'All Tests':
IntelliJ nám pekne vizuálne ukáže priebeh testov a taktiež výsledky:
Chybový test
Skúsme si teraz urobiť v kalkulačke chybu a zakomentujme vyvolávanie výnimky pri delení nulou:
fun vydel(a: Double, b: Double): Double { // if (b == 0.0) // throw IllegalArgumentException("Nemožno deliť 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, Testovanie v Kotline - Hamcrest, TestRules a best practices , sa pozrieme na zmeny v JUnit, uvedieme si knižnicu Hamcrest, naučíme sa používať pravidlá a spomenieme tie najdôležitejšie best practices na 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é 2x (11.15 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Kotlin