IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 2 - Testování v Kotlin - První unit test pomocí JUnit

V minulé lekci, Úvod do testování softwaru v Kotlin, jsme si uvedli základní typy testů a 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 Kotlin tutoriálu věnovaném testování si vytvoříme jednoduchou kalkulačku a pokryjeme ji unit testy. Zopakujeme si, co jednotkovými (unit) testy testujeme a ukážeme si, jak je do naší aplikace přidat.

Použití unit testů

Víme, že testy píšeme vždy na základě návrhu, nikoli implementace. Kontrolujeme jimi tedy očekávanou funkčnost. Pokud takovou funkčnost definuje zákazník, sáhneme po akceptačních testech. Může být také definována programátorem, který specifikuje, jak se má daná metoda chovat. Ve druhém případě použijeme unit testy, které testují detailní specifikaci aplikace, tedy její třídy.

Pamatujme, že nikdy nepíšeme testy podle toho, jak je něco uvnitř naprogramované! Velmi jednoduše by to mohlo naše myšlení svést. Například bychom zapomněli, že metodě mohou přijít 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 netestujeme jednoúčelové metody, které např. ve formulářových aplikacích pouze něco vybírají z databáze. Abychom byli konkrétnější, nemá valný smysl testovat metodu jako je tato:

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 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. 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. Pokud používáme nějakou kvalitní knihovnu, staženou např. z GitHubu, nalezneme u ní také testy, které se nejčastěji vkládají do složky src/test/ v adresářové struktuře projektu.

Jestliže 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.

Jednoduchá kalkulačka s testy

My si nyní podobnou třídu vytvoříme a zkusíme ji otestovat. Bude to pouze jednoduchá kalkulačka, která bude umět:

  • sčítat,
  • odčítat,
  • násobit,
  • dělit.

Vytvoření projektu

V praxi by ve třídě byly nějaké složitější výpočty, ale tím se zde zabývat nebudeme. Nový projekt nazveme UnitTesty a přidáme do něj třídu Kalkulacka s následující implementací:

/**
 *
 * 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("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í u desetinných čísel v Kotlin je vrácení hodnoty Infinity (nekonečno), což v aplikaci není vždy to, co uživatel očekává.

Generování testů

Pro testy používáme Java framework JUnit, který je součástí IntelliJ. Pravým tlačítkem klikneme na název třídy a zvolíme Generate:

Přidání JUnit testu v Kotlinu - Testování v Kotlin

V následujícím dialogovém okně vybereme položku Test:

Přidání JUnit testu v Kotlinu - Testování v Kotlin

V posledním okně zadáme název testu. Konvenčně se za název testované třídy připojí slovo Test, v našem případě vytvoříme třídu KalkulackaTest. Zaškrtneme také možnosti setUp a tearDown pro vygenerování potřebných metod. Jejich význam si vysvětlíme později:

Dokončení přidání unit testu v Kotlinu - Testování v Kotlin

Potvrdíme a vygeneruje se nám nový soubor s následujícím kódem:

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 třída a jednotlivé testy reprezentují metody. Dokonce zde nalezneme dvě předpřipravené metody, které jsou označené anotacemi.

Metody setUp() a tearDown()

Metody setUp() a tearDown() 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 nezávislé. Obvykle tedy před každým testem připravujeme znovu to samé prostředí, aby se vzájemně vůbec neovlivňovaly.

Do třídy si přidejme proměnnou kalkulacka a v metodě setUp() do ní vytvoříme vždy novou instanci pro každý test. Pokud by ji bylo ještě třeba dále nastavovat nebo vytvořit další závislosti, byly by také v této metodě:

class KalkulackaTest {

    private lateinit var kalkulacka: Kalkulacka

    @BeforeEach
    fun setUp() {
        kalkulacka = Kalkulacka()
    }

    @AfterEach
    fun tearDown() {
    }

}

Pokrytí třídy testy

Máme vše připraveno k přidání samotných testů. Jednotlivé metody budou vždy označené anotací @Test a budou testovat jednu konkrétní metodu z třídy Kalkulacka, typicky pro několik různých vstupů.

Metody označujeme anotacemi, abychom měli možnost vytvořit si i pomocné metody, které budeme v daném testu využívat a které nebudou pokládány za testy. IntelliJ nám totiž metody s anotací @Test automaticky spustí a vypíše jejich výsledky.

Ve starších verzích JUnit musely místo anotací metody začínat textem test a třída dědila ze třídy TestCase.

Přidejme následujících pět metod:

@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)
    }
}

K porovnávání výstupu metody s očekávanou hodnotou používáme assert metody, staticky naimportované z balíčku org.junit.jupiter.api.Assertions. Nejčastěji asi použijeme assertEquals(), která přijímá jako první parametr očekávanou hodnotu a jako druhý parametr hodnotu aktuální.

Protože jsou desetinná čísla v paměti počítače reprezentována binárně, způsobí to určitou ztrátu jejich přesnosti a také určité obtíže při jejich porovnávání. V tomto případě musíme tedy zadat i třetí parametr a to je delta, tedy kladná tolerance. Určíme tak, o kolik se může očekávaná a aktuální hodnota lišit, aby test stále prošel.

Důležité je také vyzkoušet různé vstupy. Sčítání netestujeme jen jako 1 + 1 = 2, ale zkusíme celočíselné, desetinné i negativní vstupy. 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. Nemusíme se zatěžovat s try-catch bloky, stačí danou část kódu pouze obalit metodou assertThrows() a uvést zde třídu 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. To si ukážeme v některé z dalších lekcí.

Dostupné assert metody

Kromě metody assertEquals() můžeme použít několik podobných:

  • Metoda assertArrayEquals() zkontroluje, zda dvě pole obsahují ty samé prvky.
  • Metodami assertTrue() a assertFalse() kontrolujeme, zda je vrácena hodnota true, resp. false.
  • Pokud očekáváme rozdílné hodnoty, použijeme metodu assertNotEquals().
  • Metoda assertNotNull() kontroluje hodnotu null.
  • Pro porovnání referencí na objekt využijeme metodu assertSame() nebo její negaci assertNotSame().

Existuje ještě metoda assertThat(), která umožňuje novější přístup k psaní assercí, ale tu si vysvětlíme až příště.

Spuštění testů

Testy nyní spustíme kliknutím pravým tlačítkem na projekt a vybráním položky Run 'All Tests':

Spuštění unit testů v Kotlinu - Testování v Kotlin

IntelliJ nám hezky vizuálně ukáže průběh testů a také výsledky:

Výsledek unit testů v IntelliJ - Testování v Kotlin

Chybový test

Zkusme si nyní udělat v kalkulačce chybu a zakomentujme vyvolávání výjimky při dělení nulou:

fun vydel(a: Double, b: Double): Double {
   // if (b == 0.0)
   //      throw IllegalArgumentException("Nelze dělit nulou!")
    return a / b
}

A spusťme znovu naše testy:

Neúspěšný výsledek testu v IntelliJ - Testování v Kotlin

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.

Příště, Testování v Kotlin - Hamcrest, TestRules a best practices, se podíváme na změny v JUnit, uvedeme si knihovnu Hamcrest, naučíme se používat pravidla a zmíníme ty nejdůležitější best practices pro psaní testů.


 

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 2x (11.15 kB)
Aplikace je včetně zdrojových kódů v jazyce Kotlin

 

Předchozí článek
Úvod do testování softwaru v Kotlin
Všechny články v sekci
Testování v Kotlin
Přeskočit článek
(nedoporučujeme)
Testování v Kotlin - Hamcrest, TestRules a best practices
Článek pro vás napsal Patrik Olšan
Avatar
Uživatelské hodnocení:
3 hlasů
Autor se věnuje vývoji softwaru, zejména mobilních aplikací
Aktivity