Unit tesztelés a gyakorlatban¶
Bevezető¶
Szoftver tesztelés során több szintű tesztelésről is beszélhetünk. Nézzük meg pl. a V-model fejlesztési modelt.
A model alján a kódolás felett található a Unit testing. Ezen tesztelés során a program legkisebb elemét, azaz moduljait teszteljük.
Unit tesztelés¶
Általában a fejlesztők írják ezeket a teszteket a saját implementált kódjukra. Tehát minden egyes legkisebb egység megírása után írhatunk rá egy unit tesztet. A legkisebb egység, azaz unit, a legtöbbször metódust jelent. Tehát a metódus működését fogjuk letesztelni. Pozitív irányba mindenképpen kell teszteket írnunk, de negatív irányban is érdemes.
Warning
Ezen a tesztelési szinten csak a legkisebb egységhez tartozó adattagokat és működést használjuk és teszteljük. Pl. ha webshopon regisztrálást akarunk tesztelni, akkor adatbázis kapcsolatot kell nyitnunk. Ezesetben az adatbázisból kinyerhető információt mockolnunk kell és a hard codeolt adatra kell letesztelnünk a metódusunkat.
Unit tesztelési technikák¶
- Tesztelési folyamat történhet automatikusan vagy kézzel
- Kézi tesztelés esetén a unit teszteket egy általunk írt tesztprogramból futtatjuk
- Automatikus tesztelés esetén a szükséges unit tesztet egy tesztelő keretrendszer (framework) futtatja különböző tesztesetekre, izolálva a unitot eredeti környezetétől
- Az elvárt eredmények kritériumait a programozónak kell definiálni
Előnyök¶
- Programhelyesség
- Korai szakaszban kiszűr sok hibát
- Helyes programozási szemléletre motivál (alacsony csatolás, magas kohézió)
- Gyakran szoros kapcsolatban áll a tervezési mintákkal és a refactoringgal, ezáltal a specifikációhoz legközelebb álló megoldást kaphatjuk (jó tesztesetek megválasztása)
Tippek¶
- Egy metódust minden lehetséges lényeges paraméterrel teszteljünk, ne csak jó paraméterekkel, próbáljuk meg felborítani a tesztelt osztály működését
- Hibajavítás, új funkcionalitás után nézzük meg, hogy még mindig megy-e a unit teszt, esetleg kell-e módosítani rajta
- A tesztosztályokat különítsük el fizikailag a kódfájloktól, de tegyük azonos csomagba/névtérbe
- A tesztosztályok neve legyen azonos a tesztelt osztály nevével, „Test” végződéssel ellátva
- Mindig az interfészen keresztül teszteljük a tényleges osztályt, amennyiben megvalósít valamilyen interfészt
- Egy tesztosztály egy kódosztályt teszteljen
- Felesleges unit tesztelni: getter/setterek, külső libek
JUnit¶
- http://www.junit.org (freeware)
- Az xUnit család tagja, Java implementáció
- Reflection-t használ a tesztek futtatásához
- Könnyen, gyorsan lehet új teszteket készíteni
- Tesztcsoportokat készíthetünk (suite)
- Több módon futtatható, azonnali eredmény
- Automatizálható, integrálható fejlesztőeszközök alá
- Nem minden esetben használható
- GUI tesztelés (korlátozottan, nem kényelmes)
- Web alkalmazások, elosztott rendszerek, SOA…
Assert metódusok¶
- Tesztelés közben ellenőrizzük, hogy a megfelelő eredményeket kapjuk-e – Assert metódusokkal
- Ha nem megfelelő a kapott eredmény, a teszteset sikertelennek minősül (failed)
- Statikus metódusok (Java 5.0 – static import)
- assertFalse(BooleanSupplier condition), assertTrue(BooleanSupplier condition)
- assertEquals(expected, actual)
- az expected és actual paraméterek minden Java-ban támogatott típust felvehetnek, a metódusnak minden típusra létezik overloadolt változata
- assertIterableEquals(Iterable<?> expectedm Iterable<?> actual)
- assertNotEquals(Object unexpected, Object actual)
- assertArrayEquals(expected[], actual[])
- assertNull(Object actual), assertNotNull(Object actual)
- assertSame(Object expected, Object actual), assertNotSame(Object unexpected, Object actual)
- assertThrows(Class
expectedType, Executable executable) - Ez a metódus akkor assertál, amennyiben az executable az elvárt típusnak megfelelő kivételt dobja.
- assertTimeout(Duration timeout, Executable executable)
- fail()
- +vannak olyan verziók, ahol az első paraméter egy hibaüzenet
Tesztek, tesztesetek definiálása¶
Az itt következő annotációk a Junit5-ben érvényesek, a kapcsolódó változatokat a Junit4 esetében zárójelben megadtuk.
- A tesztosztály, tesztesetek publikusan legyenek
- A metódusokat annotációkkal látjuk el
- @Test
- a metódus egy teszteset (test case)
- opcionális paraméterek: expected, timeout
- ha lefut, a teszteset sikeresnek számít (nincs assert, exception)
- @ParameterizedTest
- Junit5-ben inkább használjuk ezt az annotációt, amennyiben paraméterezett tesztet futtatunk
- @ValueSource annotációval párban
- Példa:
1 2 3 4 5 6 7 8 9 10
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import static org.junit.jupiter.api.Assertions.assertTrue; class JUnit5Test { @ParameterizedTest @ValueSource(strings = { "cali", "bali", "dani" }) void endsWithI(String str) { assertTrue(str.endsWith("i")); } }
- @RepeatedTest
- A Junit5 lehetőséget ad a teszt ismételt futtatására is.
- Példa:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.RepetitionInfo; import org.junit.jupiter.api.TestInfo; import static org.junit.jupiter.api.Assertions.assertEquals; class JUnit5Test { @RepeatedTest(value = 5, name = "{displayName} {currentRepetition}/{totalRepetitions}") @DisplayName("RepeatingTest") void customDisplayName(RepetitionInfo repInfo, TestInfo testInfo) { int i = 3; System.out.println(testInfo.getDisplayName() + "-->" + repInfo.getCurrentRepetition() ); assertEquals(repInfo.getCurrentRepetition(), i); } }
- @DisplayName
- Egyedi név deklarálása (ld. az előző példában), amely a futtató környezetben és a reportban is megjelenik.
- @BeforeEach/AfterEach (Junit4-ben: @Before/@After)
- ez a metódus lefut MINDEN teszteset hívása előtt/után
- @BeforeAll/AfterAll (Junit4-ben:@BeforeClass/@AfterClass)
- a metódus lefut EGYSZER a tesztelés kezdetén/végén
- @Tag
- Lehetőség van cimkéket rendelni a tesztekhez, amely később szűrésre is használható.
- @Disabled (Junit4-ben:@Ignore)
- csak @Test-tel párban: ezt a tesztesetet nem futtatja le
Tesztek futtatása¶
- Statikusan: run() metódusokkal saját kódból
- Dinamikusan: TestRunner osztályok segítségével
- Konzolos TestRunner:
- java junit.textui.TestRunner
- java junit.textui.TestRunner
- Eclipse integráció:
- Jobb klikk a tesztosztályon: Run As | JUnit Test
- Legtöbb Eclipse verzióban már alapból benne van a JUnit plugin
JUnit 5 használata a gyakorlatban¶
Példa feladat¶
Grade (Histogram) Számoljuk össze a névsor és a kapott pontok alapján, hogy hányan teljesítették a tárgyat elégségesen, kiemelkedően, stb.
Névsor és pontok:
Pista | 1 |
---|---|
Peti | 100 |
Laci | 90 |
Zoli | 50 |
Gergő | 10 |
András | 12 |
Megoldása
- Létrehozunk egy hisztogramot.
- Belerakunk egy-két vödröt.
- Feltöltjük elemekkel.
- Kiíratjuk a vödrök nagyságát
Tehát, az első vödör 2 elemű lesz: Pista, Gergő. A második 1 elemű: Zoli. A harmadik pedig 2 elemű: Peti és Laci.
Ezt megírjuk, lefuttatjuk. Látjuk, hogy ezekre működik, de minden egyes inputra jól működik a program? Mi történik ha valaki 100 pontnál többet kap, vagy éppen 0 pontnál kevesebbet?
Teszteljük le a programot¶
Tesztelés célja
A tesztelés elsődleges célja hibás viselkedés kimutatása. A letesztelt program nem tökéletes. Ha nem találtál hibát akkor a tesztelés sikertelen.
Hogyan írjunk unit tesztet?¶
Gyakorlati tippek
- Kipróbálás alapján Main.main(String[ ] args)
- Beégetett értékekkel teszteljünk
- "alma", 123, True
- r . nextInt (201), sort (5, 3, 4, 2), LocalDateTime.now()
- Kivételes viselkedést is teszteljük (hibás, nem várt input. . . ) assertThrows( exceptionClass , act)
- Azt is igazoljuk, hogy a program nem csinálja azt, amit nem kéne histogram.addBucket(new Limit(0, 15)); histogram.addBucket(new Limit(0, 15));
- Teszteseteket meg kell tudni ismételni, hibás esetben is „beírtam valamit és nem működött ez az izé” – egy névtelen felhasználó
Készítsünk unit tesztet¶
Feladat
Alakítsuk át a Main.main( ) metódust unit tesztté
Kód átemelése unit teszt metódusba:
Másoljuk át a kódot egy unit teszt fájlba. Minden egyes teszt metódus elé a @Test annotációt ki kell tennünk
Nem-determinisztikus hívások kicserélése beégetett értékekre:
Tehát random értékek helyett konkrétakat írjunk. Ezt azért érdemes (és muszáj) megtennünk, mivel ha találunk hibát annak reprodukálhatónak kell lennie.
Kiíratás helyettesítése megfelelő assert metódussal
A tesztelő keretrendszer (pl. JUnit) ezzel ellenőrzi, hogy jó-e a metódus
Megoldás¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Unit tesztek szerkezete¶
Arrange¶
- Tesztelendő környezet előkészítése
- Objektum példányok létrehozása
- Adattagok beállítása
- @BeforeEach @AfterEach annotációk használatosak a fenti háromhoz, melyekkel megmondhatjuk, hogy a teszt metódusok előtt és után mik történjenek, ha minden teszt előkészítése azonos!
- Bemeneti paraméterek definiálása
Act¶
- Tesztelendő rész végrehajtása
- Tesztelendő metódus hívása
- Viselkedés és visszatérési értékek rögzítése
Assert¶
- Teszt kiértékelése
- Kapott és elvárt eredmények összehasonlítása
- A unit tesztelés egyik legfontosabb része, ugyanis ezzel ellenőrizzük le, hogy jó-e a kódunk.
- Számos assert létezik: ellenőrizhetünk stringeket, booleaneket, inteket, listákat, stb. Pl.: assertEquals(double expected, double actual) assertNull(java.lang.Object object) assertTrue(boolean condition)
- Link az assertek fajtáira.
- A tetszőlegesen választott IDE fel fogja ajánlani az assertek listáját ha elkezdjük begépelni azt, hogy assert.
Elvárt és kapott eredmények, viselkedések¶
Explicit jelezzük a szándékunk
1 2 3 |
|
Programozó vagy tesztelő?
- alacsony szintű követelmények jelzése: assert expression ; pl.: egy változó nem lehet üres string
- teszt eredmények ellenőrzése: org. junit . jupiter . api . Assertions
Feladat
- Alakítsuk át a korábbi tesztet az AAA mintának megfelelően.
- kommentezéssel jelöljük az egyes részeket
- alkalmazzunk megfelelő változó neveket
Elvárt hiba
- Kivételes viselkedés tesztelése
1 2 3 4 |
|
Helytelen bemenet
1 2 |
|
Feladat A testAddBucket_invalidLimit() alapján javítsuk ki a converter.Limit oszály konstruktorát.
Egy metódus – Több teszt¶
Input „osztályonkénti” tesztelés
1 2 3 |
|
alsó metszés | histogram.addBucket(new Limit(−10, 50)) |
felső metszés | histogram.addBucket(new Limit(50, 110)) |
mindkettő metszés | histogram.addBucket(new Limit(−10, 110)) |
- Dummy, stub és mock
További unit tesztelés tippek¶
- Egy metódust minden lehetséges lényeges paraméterrel teszteljünk, ne csak jó paraméterekkel, próbáljuk meg felborítani a tesztelt osztály működését
- Például, ha valakinek a születési évét kéri a függvény, akkor ellenőrizzük le, hogy 1900 alatt mit ad vissza. Ugyanígy leellenőrizhetjük nem egész számokra is.
- Hibajavítás, új funkcionalitás után nézzük meg, hogy még mindig megy-e a unit teszt, esetleg kell-e módosítani rajta
- Futtassuk le újra Run As JUnit gombbal. Ha mindegyik zöld, akkor örülünk. Ha valamelyik piros akkor mégjobban. Ugyanis a piros jelzi azt, hogy találtunk hibát a programban, amit tudunk javítani, mielőtt a felhasználóhoz került volna.
- A tesztosztályokat különítsük el fizikailag a kódfájloktól, de tegyük azonos csomagba/névtérbe
- Tehát csináljunk egy „test” mappát és ide rakjuk a teszt fájlokat azaz a unit tesztjeinket.
- A tesztosztályok neve legyen azonos a tesztelt osztály nevével, „Test” végződéssel ellátva
- Például ha a tesztelni kívánt metódus neve az, hogy „saveFile” akkor a teszt metódusunk neve legyen: „saveFileTest”
- Mindig az interfészen keresztül teszteljük a tényleges osztályt, amennyiben megvalósít valamilyen interfészt
- Egy tesztosztály egy kódosztályt teszteljen
- Felesleges unit tesztelni: triviális megvalósításokat, külső libek Getter setterekben ha nincsen különösebb logika, akkor fölösleges letesztelni.
- Külső libek nem unitok, ezért nincs értelme unit tesztelni, mert azokat a lib fejlesztői már letesztelték.
- Ha egy metódus egy külső lib függvényét használja, akkor mockoljuk.
- Mockolás: megadjuk, hogy bizonyos helyzetekben hogy viselkedjen a meghívandó külső függvény.
Demonstráció¶
Az órához kapcsolódó projekt demonstrációs feladat itt érhető el.
Feladat¶
- Válasszunk ki a projektünkből egy tetszőleges funkcionalitást!
- Készítsünk unit tesztet a tanultak alapján!
- Válasszunk ki egy tetszőleges viselkedés alapú funkcionalitást (dinamikus működés)!
- Készítsünk mock osztályt a fenti funkcionalitáshoz, amely szimulálja a működést visszaadott értékekkel!
- Készítsünk unit tesztet az adott osztályhoz!
- Futtassuk le a teszteket, vizsgáljuk meg a lefedettség értékeket!