Figyelem: A gyakorlatok során törekedjen mindenki arra, hogy professzionális jellegű munkát végezzen (pl. kerüljük az asdfg commit megjegyzéseket, nem sokkal több idő azt írni helyette, hogy Add acceleration feature vagy Fix #5).

Bevezető

A dinamikus ellenőrzési technikák legfőbb jellemzője, hogy a vizsgált forráskód a folyamat során végrehajtásra kerül. Ide tartozik a szoftvertesztelés is, amely a fejlesztési folyamat minden szintjén megjelenik. A legkorábbi szinten a fejlesztéssel párhuzamosan vagy közvetlenül azután történő tesztelési eljárás a unit tesztelés. Jelen gyakorlat a unit tesztelésre fókuszál.

A unit általánosságban a kód egy logikailag jól szeparálható része. Ez objektumorientált szoftverek esetén legtöbbször egy vagy néhány osztály együttesét jelenti. A unit legtöbb esetben egy jól definiált interfésszel rendelkezik, amelyen keresztül elérhető annak funkcionalitása. Ez egy kiemelten fontos aspektus egy szoftver tesztelhetősége szempontjából.

A unit tesztelés célja, hogy a fejlesztés során detektáljuk és javítsuk a felbukkanó hibákat (ez a legalacsonyabb a tesztelési szintek közül, lásd vízesés modell). A hibák korai, még a fejlesztés során történő felismerésével növelhető az elkészülő rendszer minősége és csökkenthetők a késői tesztelésből eredő többletköltségek. A unit tesztelése általában önállóan történik, izoláltan a többi egységtől. Ennek több előnye is van:

Egy darab unit teszt egy jól behatárolt funkcionalitást tesztel, gyakorlatilag egy viselkedési szerződést megadva a tesztelés alatt álló egység számára.

A gyakorlat során a már meglévő, továbbfejlesztett Spaceship projektet fogjuk használni. A cél a GT4500 osztály unit tesztelése több lépésen keresztül, iteratívan felépítve.

Izoláció megvalósítása

A korábbi gyakorlatok során találkozhattunk azzal a problémával, hogy a projekt tesztjei néha sikeresen lefutnak, ám néha sikertelenül futnak (ez tipikus esete az előadáson látott "instabil teszt" nevű test smell-nek). Ennek az az oka, hogy a TorpedoStore osztálynak a viselkedése nem-determinisztikus. Így most a megbízható tesztelési környezet kialakításának első lépéseként a GT4500 osztályt izoláljuk, így később már annak a unit tesztelésére koncentrálhatunk. Tehát a TorpedoStore osztályt külső függőségként kezeljük. Ez a unit tesztelés során azt implikálja, hogy az ilyen típusú objektum felé történő hívásokat helyettesíteni, izolálni kell. A gyakorlaton a Mockito eszköz segítségével valósítjuk ezt meg.

Az izolációt használva kétféleképp is tudunk ellenőrzéseket definiálni a tesztelés alatt álló unitunk felé. Egyrészt tudunk továbbra is állapotot vizsgálni azaz, hogy a teszt lefutása után milyen állapotba kerül a tesztelt objektum. A másik lehetőség, -- amelyre a mockok adnak lehetőséget -- hogy a tesztelt unitunk interakcióit ellenőrizzük (milyen hívásokat, milyen argumentumokkal intéz a környezete, függőségei felé).

A tesztelt unitunk izolációjának azonban számos akadálya lehet. Köztük az egyik legismertebb probléma a tesztelhetőség. Ez a gyakorlaton használt GT4500 osztályban is fennáll.

  1. Bővítsd a GT4500 osztályt úgy, hogy a TorpedoStore helyettesítő objektumai injektálhatóak legyenek a tesztelt osztályba (ld. dependency injection)!
  2. Egészítsd ki a már meglévő GT4500Test tesztosztály inicializáló logikáját (init függvény) úgy, hogy a GT4500 objektum létrehozása során a TorpedoStore-t helyettesítő mock objektumokat adjon át! A helyettesítő objektumok létrehozását a Mockito mock nevű metódusának segítségével kell megvalósítani.
  3. Futtasd le a meglévő két tesztesetet! Mi történik a háttérben? Definiáld a megfelelő viselkedést a mockokhoz, majd futtasd újra a teszteseteket!
  4. Alakítsd át teszteket úgy, hogy interakciót ellenőrizzenek a mockok segítségével (verify).

Alább látható egy egyszerű példakód egy másik projektből, hogy hogyan kell a Mockito metódusait használni.

public class PriceServiceTest {
    private DataAccess mockDA;
    private PriceService ps;

    @Before public void init() {
        // Create mock for the dependency DataAccess
        mockDA = mock(DataAccess.class);
        ps = new PriceService(mockDA);
    }

    @Test public void SuccessfulPriceQuery() {
        // Arrange
        // Set the behavior of the mock: if it is called with
        //  parameter "A100" then return the value 50.
        when(mockDA.getProdPrice("A100")).thenReturn(50);

        // Act
        ps.getPrice("A100");

        // Assert
        // Verifying the mock: getProdPrice was called only once
        verify(mockDA, times(1)).getProdPrice("A100");
    }
    …
}

Teszttervezés

Unit tesztek tervezésére általában három megközelítést alkalmaznak.

Tervezz meg papíron legalább 5 tesztesetet a GT4500 osztály fireTorpedo metódusához a metódus specifikált viselkedése alapján. A tesztek tervezése során csak a fejkommentben lévő szöveges leírást használd, magát a forráskódot ne!

Tesztesetek implementációja

  1. Implementáld az új teszteseteket JUnit segítségével! Használd a helyettesítő TorpedoStore mock objektumokat a tesztesetekben lévő ellenőrzéseknél (ld. mock példa)! Próbálj állapotokat és interakciót is ellenőrizni a mockok segítségével (segítség itt)!
  2. Tervezz és implementálj legalább egy új tesztesetet pusztán a forráskód felhasználásával. Mit tapasztalsz, mik a megközelítés nehézségei?

Kódfedettség mérése

Unit tesztelés során elengedhetetlen a folytonos visszacsatolás a tesztek által elért kódfedettségről. A fedettség mérésére minden programnyelvre léteznek megoldások. Java esetében a legismertebb eszközök: JaCoCo, Cobertura, Clover.

Jelen gyakorlat során a JaCoCo eszközt használjuk, amely egy Maven plugin segítségével bárki által könnyedén telepíthető. A JaCoCo működéséhez az alábbi kódrészleteket kell a pom.xml fájlba illeszteni.

<dependency>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.3</version>
  <scope>test</scope>
</dependency>
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.3</version>
    <executions>
        <execution>
            <id>default-prepare-agent</id>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>default-report</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>
  1. Mérd meg a tesztesetek aktuális kódfedését JaCoCo segítségével! A Maven plugin csak bizonyos fázisokban aktiválódik, így a parancssorból mvn verify segítségével futtatva fog elindulni a fedettség mérése a tesztfuttatások során.
  2. Az elkészült jelentés megtekintéséhez a /target/site/jacoco mappába kell navigálni, és megnyitni az index.html fájlt. Jegyezd fel a nem fedett kódrészeket!
  3. Amennyiben a tesztesetek kódfedése nem 100%-os, tervezd meg és implementáld a hiányzó eseteket az előzőek alapján!

(Opcionális) Parametrikus tesztek

A JUnit lehetőséget ad paraméterezett tesztek létrehozására. Ennek segítségével több, hasonló viselkedést vizsgáló teszteset összevonható egy parametrikus tesztté plusz a bemeneti halmazzá, így a tesztekhez tartozó kódbázis nagymértékben csökkenthető.

Alakítsd át a meglévő 6 teszteset egy parametrikus tesztté! Segítség az elinduláshoz itt.

További olvasnivalók