JUnit tesztelés
Unit tesztelés¶
Ahhoz, hogy biztosak lehessünk abban, hogy a kód, amit írunk megfelelően működik, szükséges ellenőriznünk, avagy tesztelnünk annak működését.
Ezt természetesen megtehetjük úgy, hogy egyszerűen kipróbáljuk a programot, és megbizonyosodunk arról, hogy látszólag azt csinálja, amit kell.
Amikor elkezdünk megvalósítani egy-egy funkcionalitást, akkor gyakran tesszük azt kezdő korunkban, hogy szimplán kipróbáljuk úgy az adott implementációt, hogy az adatainkat kézzel beolvassuk valahogy, majd a képernyőre kiiratott eredményt mi magunk ellenőrizzük.
Sokkal egyszerűbb azonban, ha valamilyen automatizált eszközt használunk erre a célra.
Amikor egy problémát specifikálunk, meg tudjuk adni azokat az elvárásokat, amiket az adott probléma megoldásával teljesíteni kell.
Az egység, vagy idegen szóval unit tesztelés célja az, hogy ezeket az elvárásokat rögzítsük általa, és a funkció kifejlesztése közben automatikusan ellenőrizni tudjuk ezek teljesülését.
Természetesen a unit tesztelésnek is van hátránya, hiszen sokkal több kódot kell megírnunk, magukat a teszteket is le kell implementálni. Ugyanakkor ha van egy megfelelően használható tesztelő keretrendszerünk, akkor azért ez nem fog gondot okozni. A befektetett idő, amit a teszt írására szántunk pedig megtérülhet az által, hogy kevesebb időt kell arra pazarolnunk, hogy a nem megfelelő módon működő kódunkben megtaláljunk egy apró hibát. Ráadásul a későbbiekben is nagy-nagy hasznát vehetjük ezeknek a teszteknek, hiszen egy későbbi módosításakor a programnak ellenőrizhetjük, hogy a korábban már jól működő részei a programnak változatlanul megfelelően működnek-e. Így a programunk minőségét könnyebben tudjuk garantálni.
JUnit¶
A JUnit egy, a Java programok számára kifejlesztett unit tesztelési keretrendszer. A JUnit a Java reflection képességét használja ki annak érdekében, hogy a Java programok saját magukat ellenőrizni tudják. A programozó számára a JUnit lehetőséget biztosít és segíti:
- saját tesztek készítését és futtatásáta,
- a program követelményeinek formalizálását és az architektúra tisztázását,
- a program írását és nyomkövetését (debugolását),
- a kód integrálását és minőségének biztosítását.
A tesztelés legfőbb terminológiáit használva megkülönböztetjük az úgynevezett unit tesztet, ami egy osztály egységeinek (unitjainak, avagy metódusainak) tesztelésére szolgál. Egy teszt eset felelős egy egyedi unit, avagy metódus tesztelésére egy adott input által. Természetesen bonyolultabb esetben lehet, sőt kell is több teszt esetet adni egy egyszerű teszt esethez. Önmagában a unit tesztek nem elegendőek arra, hogy teljes mértékben validálják a program helyes működését. Kiegészítheti ezeket az integrációs tesztelés, ahol azt teszteljük, hogy a rendszer egyes elemei (osztályok és metódusok) hogyan dolgoznak együtt, de ez már nem része a JUnitnak. Így ezzel egyelőre nem is foglalkozunk.
A jelenleg elérhető legújabb JUnit a JUnit 5, amely magába foglalja a JUnit platformot, a JUnit Jupiter és JUnit Vintage modulokat. A JUnit 5 tesztek eltérnek a JUnit 4-ben használt tesztektől, azonban azt elmondhatjuk, hogy a JUnit 4 tesztjei futtathatóak a JUnit 5 keretrendszer alatt is, elvárás csupán az, hogy a JUnit 5 már legalább JDK 8-at elvár, szemben a JUnit 4 JDK 5-ös elvárásával. Mivel manapság azért a JUnit 5 elvárása a legtöbb esetben teljesül, illetve a kurzusnak nem célja megismertetni a JUnit tesztelésben rejlő összes lehetőséget, így a továbbiakban mi a JUnit 5 alapjait ismertetjük csak.
Assert osztály metódusai¶
A unit tesztelés folyamata során a feladatunk az, hogy meghívjuk a tesztelendő metódust bizonyos paraméterekre, majd megbizonyosodjunk arról, hogy a kapott eredmények az elvárt viselkedésnek megfelelőek. Ehhez úgynevezett asserteket írunk, amelyek ellenőrzik a kívánt viselkedést, tulajdonságot.
Az assert metódus nem más, mint a JUnitnak egy olyan metódusa, amely egy ellenőrzést képes megvalósítani, és abban az esetben, ha az ellenőrzés sikertelen, egy AssertionError kivételt dob, ami jelzi, hogy az adott teszt eset kiértékelése elbukott. Természetesen a programot addig kell javítani, illetve a teszteket újra futtatni, míg valamennyi teszt hiba nélkül le nem fut.
Amikor egy teszt elbukik, és dob egy AssertionError kivételt, akkor a JUnit keretrendszer ezt a hibát elkapja, és jelzi a programozó felé.
Assert metódusok fajtái:
Metódus | Leírás |
---|---|
void assertTrue(boolean test, [message]) |
Ellenőrzi, hogy a logikai feltétel igaz-e. |
void assertFalse(boolean test, [message]) |
Ellenőrzi, hogy a logikai feltétel hamis-e. |
void assertEquals(expected, actual, [message]) |
Az equals metódus alapján megvizsgálja, hogy az elvárt és a tényleges eredmény megegyezik-e. |
assertEquals(expected, actual, tolerance, [message]) |
Valós típusú elvárt és aktuális értékek egyezőségét vizsgálja, hogy belül van-e tűréshatáron. |
assertArrayEquals(expected[], actual[], [message]) |
Ellenőrzi, hogy a két tömb megegyezik-e. |
assertNull(object, [message]) |
Ellenőrzi, hogy az ojektum null -e. |
assertNotNull(object, [message]) |
Ellenőrzi, hogy az objektum nem null -e. |
assertSame(expected, actual, [message]) |
Ellenőrzi, hogy az elvárt és a tényleges objektumok referencia szerint megegyeznek-e. |
assertNotSame(expected, actual, [message]) |
Ellenőrzi, hogy az elvárt és a tényleges objektumok referencia szerint nem egyeznek-e meg. |
fail([message]) |
Feltétel nélkül elbuktatja a metódust. Annak ellenőrzésére használhatjuk, hogy a kód egy adott pontjára nem jut el a vezérlés, de arra is jó, hogy legyen egy elbukott tesztünk, mielőtt a tesztkódot megírnánk. |
Annotációk¶
A JUnit 5 különböző annotációk segítségével teszi egyszerűbbé és rugalmasabbá a tesztek megírását. A leggyakoribb teszt annotációkkal megadhatjuk az egyes metódusok szerepét a tesztelés során:
Annotáció | Leírás |
---|---|
@Test | Az adott metódus teszt metódus. |
@BeforeEach | Az adott metódus lefut minden olyan metódus előtt lefut, amely a @Test, @RepeatedTest, @ParametrizedTest vagy @TestFactory annotációkkal vannak ellátva, szerepe a teszt esetek inicializálása. |
@AfterEach | Az adott metódus lefut minden olyan metódus után lefut, amely a @Test, @RepeatedTest, @ParametrizedTest vagy @TestFactory annotációkkal vannak ellátva, feladata az ideiglenes adatok törlése. |
@BeforeAll | Az adott metódus egyszer fut le még azelőtt, hogy bármelyik @Test, @RepeatedTest, @ParametrizedTest vagy @TestFactory annotációkkal ellátott metódus futna, illetve az azokat megelőző @BeforeEach metódusok előtt. Itt lehet egyszeri inincializációs lépéseket megtenni. Ezzel az annotációval ellátott metódusnak statikusnak kell lennie. |
@AfterAll | Az adott metódus egyszer fut le azután, miután mindegyik @Test, @RepeatedTest, @ParametrizedTest vagy @TestFactory annotációkkal ellátott metódus lefutott, illetve azok @AfterEach metódusai lefutottak. Egyszeri tevékenységet ellátó utasítások helye, amelyek általában a @BeforeAll metódus által allokált erőforrások felszabadítására hivatottak. Ezzel az annotációval ellátott metódusnak statikusnak kell lennie. |
@Disabled | Adott teszt metódus letiltása. |
Ha valaki JUnit 5 helyett Junit 4 teszteket írna, akkor ezekhez nagyon hasonló annotációkat tud használni, az elnevezésekre azonban ügyelni kell.
Példa¶
Annélkül, hogy most ténylegesen egy Java osztályt tesztelnénk, készítsünk egy olyan tesztet, amin az alapvető tesztelési elemeket megfigyelhetjük és kipróbálhatjuk.
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class StandardTests {
@BeforeAll
static void setUpAll() {
System.out.println("setUpAll method is running.");
}
@BeforeEach
void setUp() {
System.out.println("setUp method is running.");
}
@Test
void succeedingTest() {
System.out.println("succeedingTest method is running.");
}
@Test
void failingTest() {
System.out.println("failingTest method is running.");
fail("a failing test");
}
@Test
@Disabled("for demonstration purposes")
void skippedTest() {
System.out.println("skippedTest method is running.");
// not executed
}
@Test
void abortedTest() {
System.out.println("abortedTest method is running.");
assumeTrue("abc".contains("Z"));
fail("test should have been aborted");
}
@AfterEach
void tearDown() {
System.out.println("tearDown method is running.");
}
@AfterAll
static void tearDownAll() {
System.out.println("tearDownAll method is running.");
}
}
Lévén JUnit 5-ös tesztet írtunk most, így a teszteléshez felhaszált legtöbb elem az org.junit.jupiter
csomagból származik, amely osztályokat így importálunk a teszt unit elején. A StandardTests
osztályban annotációkkal megadjuk azokat a metódusokat, amelyek minden teszt előtt/után lefutnak, és megadjuk azokat is, amelyek mindegyik tesztet meg kell előzzék, le kell zárják. Érdekességképp az elnevezésekben (setUp
, tearDown
) megőriztük a korábbi (JUnit 3-as vagy előtti) elnevezéseket, ahol még az annotációk helyett a polimorfizmus adta lehetőségekkel oldották meg ezen inicializáló függvények megfelelő időpontban történő meghívását.
Összesen 4 teszt metódust valósítunk meg a példában. A succeedingTest
metódus mivel ellenőrzést sem tartalmaz, így minden gond nélkül sikeresen kell lefusson. A failingTest
a fail
hívása által elbuktatja a tesztet. A skippedTest
olyan teszt, amit aktuálisan nem szeretnénk futtatni, ezt érjük el a @Disabled
annotáció által. Fejlesztés során előfordulhat, hogy van olyan ismert hiba, ami miatt egy teszt még nem tud lefutni, ilyenkor használhatjuk ezt a megoldást annak érdekében, hogy jobban tudjunk a működő funkcionalitások helyességére koncentrálni. Végezetül az abortedTest
-ben egy olyan esetet mutatunk, ahol assert
helyett egy assume
utasítást használunk. Látszólag a két dolog ugyanazt csinálja, ha a feltételünk nem teljesül, a teszt végrehajtása befejeződik, hiba jelentkezik. Ugyanakkor fontos megkülönböztetni ezeket az eseteket. Az assume
általában egy különálló feltételt vizsgál, amely ha nem teljesül, nincs értelme futtatni a tesztet, az funkcionalitásban nem lesz tesztelhető, ha ez az utasítás elbukik, akkor a teszt státusza is aborted
lesz, failed
helyett. Az assert
ezzel szemben tényleg funkcionalitást fog tesztelni.
Amikor elkészült egy JUnit teszt fájl, nyilvánvalóan szeretnénk azt fordítani, és futtatni is. Az IDE eszközök biztosítják azt számunkra a legtöbb esetben, hogy ezt könnyedén megtegyük, maguk a tesztek is könnyen fordíthatóak. A build rendszerekhez is könnyedén hozzáadhatjuk a junit függőségeket.
Parancssori futtatáshoz a Console Launcher használható, ehhez szükséges, hogy letöltsük a junit-platform-console-standalone-x.y.z.jar
(most junit-platform-console-standalone-1.8.2.jar
) állományt, aminek segítségével fordíthatjuk a tesztünket:
Most itt feltételezzük, hogy egy könyvtárban van a teszt és a jar. Majd futtathatjuk a launchert:
A launchert a különböző opcióival teljesen igényeink szerint igazíthatjuk, és tetszőleges módon futtathatjuk általa tesztjeinket.
A tesztek futása után szépen láthatjuk a tesztfuttatás hibáit és statisztikáit is:
Kimenet
setUpAll method is running.
setUp method is running.
succeedingTest method is running.
tearDown method is running.
setUp method is running.
failingTest method is running.
tearDown method is running.
setUp method is running.
abortedTest method is running.
tearDown method is running.
tearDownAll method is running.
...
Test run finished after 81 ms
[ 3 containers found ]
[ 0 containers skipped ]
[ 3 containers started ]
[ 0 containers aborted ]
[ 3 containers successful ]
[ 0 containers failed ]
[ 4 tests found ]
[ 1 tests skipped ]
[ 3 tests started ]
[ 1 tests aborted ]
[ 1 tests successful ]
[ 1 tests failed ]
Példa 2.¶
Annak érdekében, hogy a JUnit tesztelésben rejlő valódi lehetőségeket is lássuk, természetesen egy osztályt, illetve annak funkcionalitását kell teszteljük. Legyen a továbbiakban egy Teglalap
osztályunk, amelyben két adattag által reprezentálni tudjuk egy-egy téglalap objektum oldalhosszait. Legyen az osztályban egy terulet
és egy kerulet
függvény, amelyek az adott Teglalap
objektumunk területét és kerületét adják vissza. Emellett legyen egy negyzete
metódus is, amely igaz értékkel tér vissza, ha az adott téglalap objektum négyzet, ellenkező esetben hamissal tér vissza.
Mindezek mellett a Teglalap
osztály rendelkezzen egy olyan nagyobb
nevű osztály metódussal is, amely a paraméterként kapott két téglalap objektum közül visszatér azon téglalap referenciájával, amelynek nagyobb a területe. Ha a két terület egyenlő, akkor az első paraméterben kapott téglalap referenciája legyen a visszatérési érték.
class Teglalap {
private double a, b;
public Teglalap() {
this.a = 0;
this.b = 0;
}
public Teglalap(double a, double b) {
this.a = a;
this.b = b;
}
public double terulet () {
return a*b;
}
public double kerulet () {
return 2*(a+b);
}
public boolean negyzete () {
return a == b;
}
public static Teglalap nagyobb(Teglalap t1, Teglalap t2) {
if (t1.terulet() >= t2.terulet())
return t1;
else return t2;
}
Írjunk ehhez az osztályhoz JUnit tesztet! Bonyolultabb osztályok esetében persze előbb érdemesebb a tesztet megírni, amelyek teszt metódusainak mindaddig el kell buknia, amíg a megfelelő funkcionalitást nem tudtuk implementálni.
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class TeglalapTests {
private static Teglalap egyikTeglalap;
private static Teglalap masikTeglalap;
@BeforeAll
static void setUpAll() {
egyikTeglalap = new Teglalap(4.0, 4.0);
masikTeglalap = new Teglalap(2.75034, 2.3699);
}
@Test
void testTerulet() {
assertTrue(egyikTeglalap.terulet() == 16.0,
"A teglalap terulete nem megfelelo");
assertEquals(6.518030766, masikTeglalap.terulet(),
"A teglalap terulete nem megfelelo");
/*FONTOS!!!
* double értékek összehasonlítására az előzőeket NE használjuk!!!*/
assertEquals(6.52, masikTeglalap.terulet(), 0.01,
"A teglalap terulete nem megfelelo");
}
@Test
void testKerulet() {
assertEquals(egyikTeglalap.kerulet(), 16.0, 0.001,
"A teglalap kerulete nem megfelelo");
}
@Test
void testNegyzete() {
assertTrue(egyikTeglalap.negyzete(), "Ez a teglalap negyzet");
assertFalse(masikTeglalap.negyzete(), "Ez a teglalap nem negyzet");
}
@Test
void testNagyobb() {
assertSame(egyikTeglalap, Teglalap.nagyobb(egyikTeglalap, masikTeglalap),
"Nem a nagyobb teglalapot valasztottad");
}
}
Fordításkor vegyük figyelembe, hogy a teszt fordításához a fordítási útvonalon kell legyen maga a Teglalap.java állomány is. Mivel a -cp kapcsolóval az alapértelmezett classpath-t módosítottuk, így most ehhez hozzá kell adnunk azt az utat, ahol a tesztelendő állomány van. Egyszerűség kedvéért ez most legyen az aktuális könyvtárunkból elérhető:
(A ;
természetesen :
lesz, ha windows helyett linuxon próbáljuk fordítani a tesztet.)
A teszt setUpAll
metódusában létrehoztunk két téglalap objektumot. Valamennyi tesztben ezeket fogjuk felhasználni. Mivel maguk a metódusok nem változtatják ezen objektumok állapotát, így nem probléma, hogy ezeket nem inicializáljuk minden teszt futtatása előtt.
A Teglalap
osztály terulet
, kerulet
és negyzete
metódusai példánymetódusok, azaz őket egy-egy objektum példányon keresztül tudjuk meghívni. Az assertTrue
és assertEquals
metódusokkal látszólagosan ugyanazt érjük el, azonban alaposabban megvizsgálva ezeket láthatjuk, hogy az assertTrue
esetében hiba esetén csak annyit tudunk meg, hogy az elvárt érték nem ugyanaz, mint a kapott érték. Ha tehát nem konkrétan logikai értékeket szeretnénk ellenőrizni, mint a testNegyzete
metódusban a negyzete
metódus tesztelésekor, akkor mindenképp érdemesebb assertEquals
ellenőrzést használni.
A Teglalap
osztály adattagjait double
típusúnak választottuk. Bár jelen esetben nem okoz gondot, ahogy a 18. és 20. sorokban ellenőriztük az elvárt és kapott értékek azonosságát, általában azért igaz az, hogy double
esetében a számítógép számábrázolásának pontatlansága miatt érdemesebb azokat az assert
metódusokat használni, amelyek csak meghatározott közelítés mellett hasonlítják a valós számokat.
A Teglalap
osztály egyetlen static
, azaz osztály metódusa a nagyobb
metódus. Mivel ez osztály metódus, ezért ezt a teszt metódusában az osztály példányosítása nélkül a Teglalap
osztályon keresztül tudjuk meghívni. A teszteléshez használt téglalap objektumok ennek a metódusnak csupán a paraméterei lesznek.