IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

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ť.
Vytvorenie projektu

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:

Pridanie JUnit testu v Kotline - Testovanie v Kotlin

V nasledujúcom dialógovom okne vyberieme položku Test:

Pridanie JUnit testu v Kotline - Testovanie v Kotlin

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:

Dokončenie pridania unit testu v Kotline - Testovanie v Kotlin

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() a assertFalse() kontrolujeme, či je vrátená hodnota true, resp. false.
  • Pokiaľ očakávame rozdielne hodnoty, použijeme metódu assertNotEquals().
  • Metóda assertNotNull() kontroluje hodnotu null.
  • Na porovnanie referencií na objekt využijeme metódu assertSame() alebo jej negáciu assertNotSame().
Existuje ešte metóda 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':

Spustenie unít testov v Kotline - Testovanie v Kotlin

IntelliJ nám pekne vizuálne ukáže priebeh testov a taktiež výsledky:

Výsledok unit testov v IntelliJ - Testovanie v Kotlin

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:

Neúspešný výsledok testu v IntelliJ - Testovanie v Kotlin

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

 

Predchádzajúci článok
Úvod do testovania softvéru v Kotlin
Všetky články v sekcii
Testovanie v Kotlin
Preskočiť článok
(neodporúčame)
Testovanie v Kotline - Hamcrest, TestRules a best practices
Článok pre vás napísal Patrik Olšan
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Autor se věnuje vývoji softwaru, zejména mobilních aplikací
Aktivity