Kihagyás

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.

kep

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
  • 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
@Test void testFill(){
  Histogramhistogram=newHistogram();
  histogram.addBucket(newLimit(0,15));
  histogram.addBucket(newLimit(16,30));
  histogram.addBucket(newLimit(50,150));
  Map<String,Integer>scores=newHashMap<>();
  scores.put("Moomintroll",0);
  scores.put("Moominpappa",20);
  scores.put("Moominmamma",10);
  histogram.fill(scores);
  assertEquals(2,histogram.getBuckets().get(0).size());
  assertEquals(1,histogram.getBuckets().get(1).size());
  assertEquals(0,histogram.getBuckets().get(2).size());
}

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
var input = ... ; //optional
var actual= ... ;
var expected = ... ; 

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
//arrange
histogram.addBucket(new Limit(0 , 100 )) ; 
//act & assert
assertThrows(IncompatibleBucketException.class , ( ) −> histogram.addBucket(new Limit(−10 , 50 ) ) ) ; 

Helytelen bemenet

1
2
//act 
histogram.addBucket(new Limit(null , null ) )

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
//arrange
histogram.addBucket(new Limit( 0 , 100 ) ) ; 
//act 
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!

Utolsó frissítés: 2023-03-07 09:18:02