Testujeme kód s TestNG

Testování kódu je důležité, abychom ověřili kvalitu softwaru, a předešli tak chybám, které mohou nastat. Na testování existuje celá řada názorů.
Zjišťovala jsem, jaké by mělo být pokrytí různých softwarových produktů, a podle tohoto článku by mělo být cílem pokrytí testy přibližně 70-80%.
Existuje  více druhů testování; v tomto článku se budeme zabývat takzvaným jednotkovým testováním, které používáme v CzechIdM. Jednotkové testy jsou takové testy, které testují pouze určitou část kódu (například metodu), nikoli funkcionalitu. Pro CzechIdM používáme k testování framework TestNG a pro analýzu pokrytí nástroj EclEmma.

TestNG a JUnit

Jedním z nejznámějších frameworků pro jednotkové testy v Javě je JUnit. TestNG je testovací framework, který je JUnit inspirován. Jeho snahou je odstranit některé nedostatky JUnit, používá se ale velice podobně. Přechod z JUnit na TestNG je proto velice snadný.

Testy v CzechIdM

Pro testovací třídy CzechIdM vedeme oddělený projekt. Testovací metody jsou označeny anotací @Test. Samotné testování pak probíhá pomocí jednoduchých příkazů assert, kterými můžeme ověřit například správnost návratových hodnot nebo předpokládané invarianty . Anotace @Test může také obsahovat parametry, jimiž lze testovací metody dělit do skupin, vynechávat z testování, určovat pořadí, v jakém se provedou, či reagovat na vyhozené výjimky.

public class PolicyUtilTest {
/**
* Otestuje, že text neobsahuje více písmen, než bylo stanoveno.
*/
@Test
public void checkMaxAlpha() {
Policy policy = new Policy();
policy.setMaxAlpha(3);

Assert.assertTrue(PolicyUtil.isTextValid("58_sd", policy, new Identity(), new ArrayList<byte[]>()));
Assert.assertTrue(PolicyUtil.isTextValid("čD", policy, new Identity(), new ArrayList<byte[]>()));
Assert.assertTrue(PolicyUtil.isTextValid("3č67979 . § á", policy, new Identity(), new ArrayList<byte[]>()));
Assert.assertFalse(PolicyUtil.isTextValid(".:fEa664s", policy, new Identity(), new ArrayList<byte[]>()));
}

/**
* Otestuje správný počet písmen.
*/
@Test
public void getNumberOfAlphaTest() {
Assert.assertEquals(PolicyUtil.getNumberOfAlpha("abc"), 3);
Assert.assertEquals(PolicyUtil.getNumberOfAlpha("a5B8c"), 3);
Assert.assertEquals(PolicyUtil.getNumberOfAlpha("2čžýŽ"), 4);
Assert.assertEquals(PolicyUtil.getNumberOfAlpha("0156 4_-)(§"), 0);
Assert.assertEquals(PolicyUtil.getNumberOfAlpha("01"), 0);
}
}

Ukázka dvou testovací metod třídy PolicyUtilTest v CzechIdM.

Příklad ilustruje, jak jednoduše otestovat metody z naší třídy PolicyUtil, a to pomocí metod assert z TestNG. Podobných testovacích metod existuje více: například assertEquals, která testuje ekvivalenci obou parametrů, assertTrue, která testuje, zda je výraz roven true, nebo assertNull, která testuje, zda je objekt roven null. Pokud všechny assert příkazy v testovací metodě projdou, považujeme metodu za úspěšně otestovanou. Pokud některý z testů neprojde, metoda v testování selhala, a je tedy potřeba analyzovat příčiny a odstranit chyby.

Spouštění testů

Testy mohou být spouštěny buď pro každou testovací třídu zvlášť, anebo pro všechny testovací třídy najednou. K tomu slouží soubor testng.xml, do něhož uvádíme všechny testy, které mají být spuštěny.

<pre><suite name="Suite" parallel="none">
<test name="All" preserve-order="false">
<classes>
<class name="eu.bcvsolutions.idm.test.data.util.PolicyUtilsTest"/>
<class name="eu.bcvsolutions.idm.test.data.dto.DTOGroupTest"/>
...
</classes>
</test>
</suite></pre>
Ukázka konfiguračního soubor testng.xml.

Mezi elementy classes jsou uvedeny jednotlivé elementy class, v jejichž atributu „name“ specifikujeme konkrétní testovací třídy.
Třídy, které v souboru testng.xml obsaženy nejsou, nebudou otestovány, přestože se v projektu nachází. Je proto důležité, aby byla nově vzniklá testovací třída zařazena do souboru testng.xml.

Psaní složitějších testů

V jistých situacích testy s jednoduchou anotací @Test nestačí. Například ve situaci, kdy pracujeme s výjimkami nebo nám záleží na pořadí, ve kterém se jednotlivé testovací metody vykonávají.

Testování výjimek

Předpokládejme, že jsme napsali metodu, která za jistých okolností vyhodí výjimku. Rádi bychom proto otestovali, zda se tak skutečně děje. K anotaci @Test proto přidáme parametr expectedExceptions, v němž je uvedena třída očekávané výjimky.

public class PolicyUtils {

public static void validatePolicy(PolicyView a, PolicyView b) {
Integer minValue = null;
Integer maxValue = null;
String nameA = (String) a.get(PolicyViewHandler.NAME_ATTRIBUTE);
String nameB = (String) b.get(PolicyViewHandler.NAME_ATTRIBUTE);

minValue = (Integer) a.get(PolicyViewHandler.MIN_ALPHA_ATTRIBUTE);
maxValue = (Integer) a.get(PolicyViewHandler.MAX_ALPHA_ATTRIBUTE);
validate(minValue, maxValue, nameA, nameB);

minValue = (Integer) a.get(PolicyViewHandler.MIN_LENGTH_ATTRIBUTE);
maxValue = (Integer) a.get(PolicyViewHandler.MAX_LENGTH_ATTRIBUTE);
validate(minValue, maxValue, nameA, nameB);

...
}

private static void validate(Integer min, Integer max, String nameA, String nameB){
if (min == null || max == null) {
return;
}

if (min > max) {
throw new IllegalArgumentException(String.format("Could not make
intersection from policies: %s and %s", nameA, nameB));
}
}
}

Metoda validate ze třídy PolicyUtils za jistých okolností vyhazuje výjimku.

public class PolicyUtilsTest {

@Test(expectedExceptions = IllegalArgumentException.class)
public void validatePolicyTest() {
PolicyView a = new PolicyView();
PolicyView b = new PolicyView();

a.put(PolicyViewHandler.NAME_ATTRIBUTE , "Nazev1");
b.put(PolicyViewHandler.NAME_ATTRIBUTE , "Nazev2");

a.put(PolicyViewHandler.MIN_ALPHA_ATTRIBUTE, new Integer(10));
a.put(PolicyViewHandler.MAX_ALPHA_ATTRIBUTE, new Integer(1));

a.put(PolicyViewHandler.MIN_LENGTH_ATTRIBUTE, new Integer(2));
a.put(PolicyViewHandler.MAX_LENGTH_ATTRIBUTE, new Integer(3));

PolicyUtils.validatePolicy(a, b);
}
}

Testovací třída PolicyUtilsTest pro třídu PolicyUtils obsahuje metodu validatePolicyTest, která očekává výjimku IllegalArgumentException

Naše testovací třída ve své metodě validatePolicyTest testuje metodu validatePolicy na třídě PolicyUtils. V jisté situaci může dojít k výjimce. Stane se tak, zadáme-li minimální hodnotě číslo větší než je hodnota maximální. V našem případě, nastavíme-li parametr min na 10 a max na 1. Do parametru expectedExceptions nastavíme naši očekávanou výjimku IllegalArgumentException. Kdybychom očekávanou výjimku neuvedli, test by neprošel.

Když záleží na pořadí testování

V jistých situacích závisí na pořadí prováděných testů.
Mějme testovací metodu A, testovací metodu B a testovací metodu C. Předpokládejme, že potřebujeme, aby se testovací metoda B spustila až tehdy, jakmile skončí testovací metoda A, a testovací metoda C aby se spustila až po ukončení metod A a B.

K tomu slouží parametr dependsOnMethods u anotace @Test, pomocí něhož uvádíme, na jaké předchozí testovací metodě naše testovací metoda závisí.

Závislost metod často využijeme u testování takzvaných CRUD (Create-Retrieve-Update-Delete) operací:

public class OrganisationManagementBeanTest {

@Test
public void organisationSimpleCreateAndRead() throws Exception {
//Kód, který testuje vytvoření nové organizace a nastavení potřebných parametrů.
}

@Test(dependsOnMethods = {"organisationSimpleCreateAndRead"})
public void organisationSimpleUpdate() throws Exception {
//Kód, který testuje update organizace.
}

@Test(dependsOnMethods = {"organisationSimpleUpdate"}, alwaysRun = true)
public void organisationSimpleDelete() throws Exception {
//Kód, který testuje smazání organizace.
}
}

Ve třídě OrganisationManagementBeanTest jsou tři metody testující CRUD operace.
Potřebujeme, aby se nejdříve vytvořila organizace, kterou později upravíme a nakonec smažeme.

Povšimněme si druhého parametru alwaysRun, který může nabývat hodnoty true nebo false. Nastavíme-li ho na true, řekneme tím, že pokud tato metoda závisí na jiném testu, který skončí neúspěchem, bude test přesto proveden. Defaultně je tento parametr nastaven na false. Neprojde-li tedy tedy předchozí metoda, nebude se testovat ani metoda., u níž je anotace uvedena. V naší třídě OrganisationManagementBeanTest je parametr alwaysRun použit, aby se naše vytvořená organizace za každých okolností smazala. Předejdeme tím „zašpinění“ databáze daty z předešlých neúspěšných testů.

Testovací třída OrganisationManagementBeanTest testuje CRUD operace.

Pokrytí testy

Při testování kódu se zpravidla měří také pokrytí testy. Pokrytím testy rozumíme poměr mezi počtem otestovaných a neotestovaných řádků kódu.

K tomu používáme nástroj EclEmma, jehož prostřednictvím spouštíme testování kódu. Po ukončení testů nástroj spustí detailní statistiku, kolik čeho bylo otestováno a jaké je celkové pokrytí.
Nástroj nám také barevně vyznačí, které části kódu byly otestovány a které ne. Zelenou barvou jsou označeny části kódu, které byly otestovány, červenou neotestované části a žlutou barvou části, které jsou otestovány jen částečně.

Cílem testování je tedy dosáhnout co nejvíce zelených řádků.

Zcela pokryté části kódu – vyznačeno zelenou barvou

Zatím nepokryté části kódu – vyznačeno červenou barvou

Závěr

V článku jste se seznámili s tím, jak pracovat s frameworkem TestNG a jak vytvářet jednoduché testy. Pokud byste se nás chtěli zeptat na něco ohledně testování nebo našeho Identity Manageru CzechIdM, neváhejte nám napsat na info@bcvsolutions.eu!

Zdroje

TestNG framework – http://testng.org/
EclEmma – http://www.eclemma.org/
Diskuse nad pokrytím kódu testy – http://www.bullseye.com/minimum.html