Unit testy v Javě a JUnit

Java Pro pokročilé Unit testy v Javě a JUnit

Testování našeho kódu je velice důležitá a v menších projektech často opomíjená část vývoje softwaru. Její účel je myslím všem jasný. Testování jako celek je docela věda a existuje mnoho přístupů a způsobů testování. To, oč nám většinou jde, je napsat automatický test pro určitou funkčnost/kom­ponentu programu. Ten poté samozřejmě můžeme spouštět neomezeně-krát stisknutím jednoho tlačítka a ověřovat tak funkčnost kódu např. po refactoringu. Dnes se zaměříme na tzv. unit testy, které slouží programátorovi jako okamžitá zpětná vazba k napsanému kódu. Konkrétně se budeme zabývat frameworkem JUnit určený pro psaní unit testů v Javě.

Unit testy

Jak už název napovídá, tyto testy budou sloužit k testování menších jednotek zdrojového kódu. Obecně by to mělo fungovat tak, že programátor napíše kód (metodu) a bezprostředně nato pro ni napíše test (dokonce existuje přístup psaní testů před kódem – tzv. Test Driven Development, není to zas taková hloupost, jak by se na první pohled mohlo zdát). Test by měl testovat chování kódu jak za standardních situací, tak v situacích mimořádných. Např. co se stane, dostane-li metoda na vstupu null.

Možná si to ani neuvědomujete, ale určitou podobu unit testů už jste určitě někdy použili. Kolikrát jste si nechali do konzole vypisovat informace o nějakých proměnných a kontrolovali, zda má správnou hodnotu? Tím v podstatě testujete svůj kód. Určitě mi ale dáte zapravdu, že takovýto způsob je dost pomalý, neefektivní, nepřehledný, neautomatický (pokaždé musíte hodnotu proměnné kontrolovat) a nezachovatelný (ten výpis poté musíte odstranit nebo zakomentovat, čímž vzniká ještě větší nepřehlednost). Při vývoji větších projektů bych ještě mohl dodat "nepoužitelný".

Pro nás teď bude důležité, že to jde líp. Co takhle oddělit testování od testovaného kódu? Pro každou naši metodu, kterou chceme otestovat, vytvořit speciální metodu v úplně jiném balíčku, a v ní testovat její funkčnost? A co vytvořit obecný standard tohoto testování tak, aby ho podporovala většina IDE a mohla nám to zjednodušit ještě víc? Nebojte, není to všechno na vás. Podobný standard už existuje a jmenuje se JUnit (existuje jich samozřejmě víc, ze známých např. TestNG; JUnit je ale nejpoužívanější).

Jednoduchý unit test

Než se vrhneme do popisování konkrétního frameworku, pojďme si podle postupu uvedeného výše ukázat, jak by takový unit test mohl vypadat. Začneme tedy třídou (komponentou), kterou chceme otestovat:

class Container {

    public static final int MAX_VALUE_LENGTH = 10;

    private String value;

    public Container(String value) {
        setValue(value);
    }


    public void setValue(String newValue) {
        if (newValue == null) throw new IllegalArgumentException("cannot set value to null");
        value = newValue.length() > MAX_VALUE_LENGTH ? newValue.substring(0, MAX_VALUE_LENGTH - 3) + "..." : newValue;
    }

    public String getValue() {
        return value;
    }
}

Je to extrémně jednoduché, nevyžadující popis. Testy chceme oddělit do jiného balíčku, čili budeme potřebovat další třídu pro testy:

class ContainerTest {

}

Nyní bude potřeba vytvořit test pro každou (důležitou) funkcionalitu této třídy:

private Container toTest;

private void prepare() {
    toTest = new Container("random");
}

public void testShortValueSetting() {
    prepare();
    // generate String of acceptable length for the container
    String value = Stream.generate(() ->String.valueOf('x')).limit(Container.MAX_VALUE_LENGTH).collect(Collectors.joining());
    toTest.setValue(value);
    System.out.println(toTest.getValue().equals(value) ? "Short value setting test passed!" : "Short value setting test failed!");
}

public void testLongValueSetting() {
    prepare();
    // generate String of not acceptable length for the container
    String value = Stream.generate(() -> String.valueOf('x')).limit(Container.MAX_VALUE_LENGTH + 1).collect(Collectors.joining());
    toTest.setValue(value);
    String shouldContain = value.substring(0, Container.MAX_VALUE_LENGTH - 3) + "...";
    System.out.println(toTest.getValue().equals(shouldContain) ? "Long value setting test passed!" : "Long value setting test failed!");
}

public void testNullSetting() {
    prepare();
    try {
        toTest.setValue(null);
        System.out.println("Null settings test failed - should throw an IllegalArgumentException!");
    } catch (IllegalArgumentException e) {
        System.out.println("Null settings test passed!");
    } catch (Throwable t) {
        System.out.println("Null settings test failed - threw another throwable!");
    }
}

Než se dostaneme k podrobnějšímu popisu – kromě mnoha špatných věcí nám tento kód ukazuje další skvělou věc testování – může posloužit jako dokumentace. Pokud by někdo nerozuměl dokonale naší testované třídě, přečte si testy a porozumí jí. Vždyť právě testy popisují každý důležitý aspekt testované komponenty. Další věc, proč je toto možné, je trochu odlišný přístup k psaní testů než kódu – zejména zjednodušení i na úkor duplicit. Testy tak obecně chápe větší procento lidí než kód samotný. O těchto jemných odlišnostech kódu v testech se ještě zmíním.

Zpět k testům – na první pohled je vidět mnoho nedostatků. Metoda prepare se musí manuálně volat před každým testem, ty výpisy do konzole nejsou moc elegantní a odchytávání výjimky už vůbec ne.

Každopádně vše je hotové. Super, co teď? Bylo by dobré testy nějak spustit. Dočasně v hlavní metodě main? Ne, to bychom zase míchali testy s funkcionalitou a jednou bychom to museli odstranit. Asi si budeme muset vytvořit vlastní metodu main a tu poté spouštět. Pro nás to nebude takový problém, ale pokud bychom měli tisíce metod a stovky tříd... Ano ano, slyším vás bědovat a vykřikovat něco o reflexi, mnohé také napadá řešení ostatních problémů s kódem. Určitě by to byla sranda, ale za chvíli si ukážeme, že hotový standard řešící všechny tyto problémy už existuje.

Vytvořme ještě tu zmíněnou metodu main a zkusme spustit naše testy:

public static void main(String[] args) {
    ContainerTest test = new ContainerTest();
    test.testShortValueSetting();
    test.testLongValueSetting();
    test.testNullSetting();
}

Výpis:

Short value setting test passed!
Long value setting test passed!
Null settings test passed!

Základní principy

Než si ukážeme JUnit, pojďme si ještě shrnout nejdůležitější principy pro psaní unit testů, které bychom měli dodržovat:

  • Izolovanost – unit testy by měly být 100% izolované od okolí a navzájem. Nemělo by je nijak ovlivnit pořadí, v jakém jsou spuštěny, nebo že zrovna nejde internet.
  • Detailní a rychlá zpětná vazba – testy by měly programátorovi rychle poskytnout co nejdetailnější zpětnou vazbu o fungování kódu, případně o chybě.
  • Jeden test, jedna funkčnost – jeden test by měl testovat pouze jednu vlastnost/funkčnost testované komponenty.
  • Pokrytí kódu testy – testy by opravdu měly pokrývat naprostou většinu funkčnosti aplikace. Je mnoho způsobů, jak se dodržení tohoto měří (vstáhnuto na testované třídy, metody, řádky kódu, podmínky atd.). Např. doporučený poměr řádky testů / řádky kódu je asi 0.8.
  • Jednoduchost – kód testů by měl být co nejjednodušší a co nejsnazší na pochopení. Určitě znáte zásadu DRY (Do not Repeat Yourself). U testů ji nahrazuje DAMP (Descriptive And Meaningful Phrases), které nadřazuje pochopitelnost nad neexistenci duplicit. Rozhodně to ale neznamená, že kód testů můžeme odfláknout!

JUnit

JUnit je nejrozšířenější framework pro vytváření unit testů v Javě. Je to přesně ten standard, o kterém jsem mluvil – každé IDE, které stojí za řeč, ho podporuje (resp. pomáhá nám s ním). Nejlepší bude ukázat si, jak by naše testovací třída vypadala s použitím JUnit (použijeme nejnovější verzi JUnit 4):

class ContainerTest {

    public Container toTest;

    @Before
    public void prepare() {
        toTest = new Container("");
    }

    @Test
    public void testShortValueSetting() {
        // generate String of acceptable length for the container
        String expected = ofSpecificLength(Container.MAX_VALUE_LENGTH);
        toTest.setValue(expected);
        assertEquals("Container hasn't set the short string properly", expected, toTest.getValue());
    }

    @Test
    public void testLongValueSetting() {
        // generate String of not acceptable length for the container
        String value = ofSpecificLength(Container.MAX_VALUE_LENGTH + 1);
        toTest.setValue(value);
        String expected = value.substring(0, Container.MAX_VALUE_LENGTH - 3) + "...";
        assertEquals("Container hasn't set the long string properly", expected, toTest.getValue());
    }

    @Rule
    public ExpectedException exception = ExpectedException.none();

    @Test
    public void testNullSetting() {
        exception.expect(IllegalArgumentException.class);
        toTest.setValue(null);
    }

    private String ofSpecificLength(int length) {
        return Stream.generate(() -> String.valueOf('x')).limit(length).collect(Collectors.joining());
    }
}

Později si podrobně vysvětlíme, jak to vše funguje. Už nyní si ale určitě dokážete udělat představu.

Pojďme si testy spustit! JUnit není součástí standardního balíčku Javy, takže si budeme muset stáhnout knihovnu třeba z junit.org/ (nebo si ji stáhněte s přílohou pod článkem). Přidáme ji do našeho projektu, a jednoduše stiskem jednoho tlačítka spustíme. Pochopitelně každé IDE se trochu liší ve spouštění a v tom, co se děje po něm. Třeba u mě v IntelliJ to vypadalo následovně:

Výsledky testů v IntelliJ

(jaký to slastný pocit)

Příště si řekneme víc o testování za pomoci JUnit.


 

Stáhnout

Staženo 19x (315.18 kB)
Aplikace je včetně zdrojových kódů v jazyce java

 

  Aktivity (3)

Článek pro vás napsal Matěj Kripner
Avatar
Autor se převážně věnuje programování a dalším věcem, které považuje za důležité.

Jak se ti líbí článek?
Celkem (7 hlasů) :
4.714294.714294.714294.714294.71429


 


Miniatura
Předchozí článek
JNI - Java Native Interface
Miniatura
Všechny články v sekci
Java - Pro pokročilé
Miniatura
Následující článek
Mockito - unit test framework

 

 

Komentáře

Avatar
martin.cernik:

Pěkný článek. Bude někdy pokračování? :-)

 
Odpovědět 20. ledna 19:10
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 1 zpráv z 1.