Zarábaj až 6 000 € mesačne! Akreditované rekvalifikačné kurzy od 0 €. Viac informácií.

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:

Vytvorenie projektu v IntelliJ IDEA - Testovanie v Jave

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:

Pridanie JUnit závislosti - Testovanie v Jave

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:

Pridanie JUnit verzie - Testovanie v Jave

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:

Create test - Testovanie v Jave

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:

Vygenerovanie testu - Testovanie v Jave

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 pomocou equals()).
  • 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:

Spustenie testov - Testovanie v Jave

IntelliJ IDEA nám pekne vizuálne ukáže priebeh testov (tie naše budú v okamihu hotové) a taktiež výsledky:

Výsledky testov - Testovanie v Jave

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:

Neúspešný výsledok testov - Testovanie v Jave

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

 

Predchádzajúci článok
Úvod do testovania softvéru v Jave
Všetky články v sekcii
Testovanie v Jave
Preskočiť článok
(neodporúčame)
Kvíz - Úvod do testovania a unit testov v Jave
Článok pre vás napísal David Hartinger
Avatar
Užívateľské hodnotenie:
2 hlasov
David je zakladatelem ITnetwork a programování se profesionálně věnuje 15 let. Má rád Nirvanu, nemovitosti a svobodu podnikání.
Unicorn university David sa informačné technológie naučil na Unicorn University - prestížnej súkromnej vysokej škole IT a ekonómie.
Aktivity