Unit testy v Jave a JUnit
Testovanie nášho kódu je veľmi dôležitá a v menších projektoch často zanedbávaná časť vývoja softvéru. Jej účel je myslím všetkým jasný. Testovanie ako celok je celkom veda a existuje mnoho prístupov a spôsobov testovania. To, o čo nám väčšinou ide, je napísať automatický test pre určitú funkčnosť / komponent programu. Ten potom samozrejme môžeme spúšťať neobmedzene-krát stlačením jedného tlačidla a overovať tak funkčnosť kódu napr. Po Refactoring. Dnes sa zameriame na tzv. Unit testy, ktoré slúžia programátorovi ako okamžitá spätná väzba k napísanému kódu. Konkrétne sa budeme zaoberať frameworkom JUnit určený pre písanie unit testov v Jave.
Unit testy
Ako už názov napovedá, tieto testy budú slúžiť na testovanie menších jednotiek zdrojového kódu. Všeobecne by to malo fungovať tak, že programátor napíše kód (metódu) a bezprostredne nato pre ňu napíše test (dokonca existuje prístup písanie testov pred kódom - tzv. Test Driven Development, nie je to zas taká hlúposť, ako by sa na prvý pohľad mohlo zdať ). Test by mal testovať správanie kódu ako za štandardných situácií, tak v situáciách mimoriadnych. Napr. čo sa stane, ak dostane metóda na vstupe null.
Možno si to ani neuvedomujete, ale určitú podobu unit testov už ste určite niekedy použili. Koľkokrát ste si nechali do konzoly vypisovať informácie o nejakých premenných a kontrolovali, či má správnu hodnotu? Tým v podstate testujete svoj kód. Určite mi ale dáte za pravdu, že takýto spôsob je dosť pomalý, neefektívne, neprehľadný, neautomatického (zakaždým musíte hodnotu premennej kontrolovať) a nezachovatelný (ten výpis potom musíte odstrániť alebo zakomentovat, čím vzniká ešte väčšia neprehľadnosť). Pri vývoji väčších projektov by som ešte mohol dodať "nepoužiteľný".
Pre nás teraz bude dôležité, že to ide lepšie. Čo tak oddeliť testovanie od testovaného kódu? Pre každú našu metódu, ktorú chceme otestovať, vytvoriť špeciálnu metódu v úplne inom balíčku, a v nej testovať jej funkčnosť? A čo vytvoriť všeobecný štandard tohto testovania tak, aby ho podporovala väčšina IDE a mohla nám to zjednodušiť ešte viac? Nebojte, nie je to všetko na vás. Podobný štandard už existuje a volá sa JUnit (existuje ich samozrejme viac, zo známych napr. TestNG; JUnit je ale najpoužívanejšie).
Jednoduchý unit test
Než sa vrhneme do popisovanie konkrétneho frameworku, poďme si podľa postupu uvedeného vyššie ukázať, ako by takýto unit test mohol vyzerať. Začneme teda triedou (komponent), ktorú chceme otestovať:
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émne jednoduché, nevyžadujúci popis. Testy chceme oddeliť do iného balíčka, čiže budeme potrebovať ďalšiu triedu pre testy:
class ContainerTest {
}
Teraz bude potrebné vytvoriť test pre každú (dôležitú) funkcionalitu tejto triedy:
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ž sa dostaneme k podrobnejšiemu popisu - okrem mnohých zlých vecí nám tento kód ukazuje ďalšiu skvelú vec testovanie - môže poslúžiť ako dokumentácia. Ak by niekto nerozumel dokonale našej testovanej triede, prečíta si testy a porozumie jej. Veď práve testy popisujú každý dôležitý aspekt testované komponenty. Ďalšia vec, prečo je toto možné, je trochu odlišný prístup k písanie testov než kódu - najmä zjednodušenie aj na úkor duplicít. Testy tak všeobecne chápe väčšie percento ľudí ako kód samotný. O týchto jemných odlišnostiach kódu v testoch sa ešte zmienim.
Späť k testom - na prvý pohľad je vidieť mnoho nedostatkov. Metóda prepare sa musí manuálne volať pred každým testom, tie výpisy do konzoly nie sú moc elegantný a odchytávanie výnimky už vôbec nie.
Každopádne všetko je hotové. Super, čo teraz? Bolo by dobré testy nejako spustiť. Dočasne v hlavnej metóde main? Nie, to by sme zasa miešali testy s funkcionalitou a jednou by sme to museli odstrániť. Asi si budeme musieť vytvoriť vlastnú metódu main a tú potom spúšťať. Pre nás to nebude taký problém, ale ak by sme mali tisíce metód a stovky tried ... Áno áno, počujem vás nariekať a vykrikovať niečo o reflexiu, mnohé tiež napáda riešenie ostatných problémov s kódom. Určite by to bola sranda, ale za chvíľu si ukážeme, že hotový štandard riešiaca všetky tieto problémy už existuje.
Vytvorme ešte tú spomínanú metódu main a skúsme spustiť 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é princípy
Než si ukážeme JUnit, poďme si ešte zhrnúť najdôležitejšie princípy pre písanie unit testov, ktoré by sme mali dodržiavať:
- Izolovanosť - unit testy by mali byť 100% izolované od okolia a navzájom. Nemalo by ich nijako ovplyvniť poradie, v akom sú spustené, alebo že práve nejde internet.
- Detailné a rýchla spätná väzba - testy by mali programátorovi rýchlo poskytnúť čo najdetailnejšie spätnú väzbu o fungovaní kódu, prípadne o chybe.
- Jeden test, jedna funkčnosť - jeden test by mal testovať iba jednu vlastnosť / funkčnosť testované komponenty.
- Pokrytie kódu testy - testy by naozaj mali pokrývať úplnú väčšinu funkčnosti aplikácie. Je mnoho spôsobov, ako sa dodržanie tohto meria (vstáhnuto na testované triedy, metódy, riadky kódu, podmienky atď.). Napr. doporučený pomer riadky testov / riadky kódu je asi 0.8.
- Jednoduchosť - kód testov by mal byť čo najjednoduchší a čo najľahšie na pochopenie. Určite poznáte zásadu DRY (Do not Repeat Yourself). Pri testoch ju nahrádza DAMP (Descriptive And Meaningful Phrases), ktoré nadraďuje pochopiteľnosť nad neexistenciu duplicít. Rozhodne to ale neznamená, že kód testov môžeme odfláknuť!
JUnit
JUnit je najrozšírenejší framework pre vytváranie unit testov v Jave. Je to presne ten štandard, o ktorom som hovoril - každé IDE, ktoré stoja za reč, ho podporuje (resp. Pomáha nám s ním). Najlepšie bude ukázať si, ako by naše testovacie trieda vyzerala s použitím JUnit (použijeme najnovšiu verziu 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()); } }
Neskôr si podrobne vysvetlíme, ako to všetko funguje. Už teraz si ale určite dokážete urobiť predstavu.
Poďme si testy spustiť! JUnit nie je súčasťou štandardného balíčka Javy, takže si budeme musieť stiahnuť knižnicu napríklad z junit.org/ (alebo si ju stiahnite s prílohou pod článkom). Pridáme ju do nášho projektu, a jednoducho stlačením jedného tlačidla spustíme. Pochopiteľne každej IDE sa trochu líšia vo spúšťanie a v tom, čo sa deje po ňom. Možno u mňa v IntelliJ to vyzeralo nasledovne:
(aký to slastný pocit)
Nabudúce si povieme viac o testovaní za pomoci JUnit.
Nabudúce si v lekcii Java spustenie - Programové argumenty predvedieme spustenie s používaním programových argumentov.
Stiahnuť
Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami
Stiahnuté 90x (315.18 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java