CodeGym /Java blog /Véletlen /Mindent az egységtesztről: technikák, fogalmak, gyakorlat...
John Squirrels
Szint
San Francisco

Mindent az egységtesztről: technikák, fogalmak, gyakorlat

Megjelent a csoportban
Ma nem fogsz találni olyan alkalmazást, amely ne lenne tele tesztekkel, így ez a téma minden eddiginél aktuálisabb lesz a kezdő fejlesztők számára: tesztek nélkül nem lehetsz sikeres. Nézzük meg, hogy milyen típusú tesztelést alkalmaznak elvileg, majd részletesen megvizsgáljuk mindazt, amit az egységtesztelésről tudni lehet. Mindent az egységtesztről: technikák, fogalmak, gyakorlat - 1

A tesztelés típusai

Mi az a teszt? A Wikipédia szerint: "A szoftvertesztelés egy szoftver- vagy rendszerkomponens végrehajtását foglalja magában egy vagy több érdekes tulajdonság értékelése céljából." Más szóval, rendszerünk helyességének ellenőrzése bizonyos helyzetekben. Nos, lássuk, milyen típusú tesztek léteznek általában:
  • Egységteszt – Olyan tesztek, amelyek célja a rendszer minden moduljának külön-külön történő ellenőrzése. Ezeket a teszteket a rendszer legkisebb atomi részeire, pl. modulokra kell alkalmazni.
  • Rendszertesztelés – Magas szintű tesztelés az alkalmazás egy nagyobb részének vagy a rendszer egészének működésének ellenőrzésére.
  • Regressziós tesztelés – Tesztelés, amellyel ellenőrizhető, hogy az új funkciók vagy hibajavítások befolyásolják-e az alkalmazás meglévő funkcióit, vagy bevezetnek-e régi hibákat.
  • Funkcionális tesztelés – Annak ellenőrzése, hogy az alkalmazás egy része megfelel-e a specifikációkban, felhasználói történetekben stb.

    A funkcionális tesztelés típusai:

    • White-box tesztelés — Annak ellenőrzése, hogy az alkalmazás egy része megfelel-e a követelményeknek, miközben ismeri a rendszer belső megvalósítását;
    • Black-box tesztelés – Annak ellenőrzése, hogy az alkalmazás egy része megfelel-e a követelményeknek a rendszer belső megvalósításának ismerete nélkül.

  • Teljesítményteszt – Olyan tesztek, amelyek annak meghatározására szolgálnak, hogy a rendszer vagy a rendszer része hogyan teljesít bizonyos terhelés mellett.
  • Terhelési tesztelés — A rendszer normál terhelés melletti stabilitásának ellenőrzésére szolgáló tesztek, valamint annak a maximális terhelésnek a megállapítása, amelynél az alkalmazás továbbra is megfelelően működik.
  • Stressz-teszt – Az alkalmazás nem szabványos terhelések melletti teljesítményének ellenőrzésére és a rendszer meghibásodása előtti maximális terhelés meghatározására szolgáló tesztelés.
  • Biztonsági tesztelés – A rendszer biztonságának ellenőrzésére használt tesztek (hackerek, vírusok, bizalmas adatokhoz való jogosulatlan hozzáférés és egyéb kellemes támadások).
  • Lokalizációs tesztelés — Az alkalmazás lokalizációjának tesztjei.
  • Használhatósági tesztelés – A használhatóság, érthetőség, vonzerő és tanulhatóság ellenőrzését célzó tesztelés.
Mindez jól hangzik, de hogyan működik a gyakorlatban? Egyszerű! Mike Cohn tesztpiramist használjuk: Mindent az egységtesztről: technikák, fogalmak, gyakorlat - 2Ez a piramis leegyszerűsített változata: most még kisebb részekre van osztva. De ma nem leszünk túl kifinomultabbak. Megfontoljuk a legegyszerűbb változatot.
  1. Egység — Ez a szakasz az egységtesztekre vonatkozik, amelyeket az alkalmazás különböző rétegeiben alkalmaznak. Az alkalmazáslogika legkisebb osztható egységét tesztelik. Például osztályok, de leggyakrabban módszerek. Ezek a tesztek általában megpróbálják a lehető legjobban elkülöníteni a tesztelteket minden külső logikától. Vagyis megpróbálják azt az illúziót kelteni, hogy az alkalmazás többi része a várakozásoknak megfelelően fut.

    Mindig sok ilyen tesztnek kell lennie (több, mint bármely más típusnak), mivel kis darabokat tesztelnek, és nagyon könnyűek, nem fogyasztanak sok erőforrást (értsd: RAM-ot és időt).

  2. Integráció – Ez a szakasz az integrációs tesztelésre vonatkozik. Ez a tesztelés a rendszer nagyobb darabjait ellenőrzi. Vagyis vagy több logikai elemet (több metódust vagy osztályt) kombinál, vagy egy külső komponenssel ellenőrzi a kölcsönhatás helyességét. Ezek a tesztek általában kisebbek, mint az egységtesztek, mert nehezebbek.

    Az integrációs tesztre példa lehet az adatbázishoz való csatlakozás, és az azzal való munkavégzés módszereinek helyes működésének ellenőrzése.

  3. UI — Ez a rész a felhasználói felület működését ellenőrző tesztekre vonatkozik. Ezek az alkalmazás minden szintjén magukban foglalják a logikát, ezért nevezik végponttól végpontig terjedő teszteknek is. Általában jóval kevesebb van belőlük, mert ezek a legnehezebbek, és a legszükségesebb (használt) útvonalakat kell ellenőrizniük.

    A fenti képen azt látjuk, hogy a háromszög különböző részei eltérő méretűek: a valós munkában megközelítőleg azonos arányok vannak a különböző típusú tesztek számában.

    Ma a legelterjedtebb teszteket, egységteszteket nézzük meg közelebbről, hiszen minden önmagát tisztelő Java fejlesztőnek tudnia kell ezeket alapszinten használni.

Kulcsfogalmak az egységtesztben

A tesztlefedettség (kódlefedettség) az egyik fő mérőszáma annak, hogy egy alkalmazást mennyire tesztelnek. Ez a kód azon százaléka, amelyet a tesztek lefednek (0-100%). A gyakorlatban sokan ezt a százalékot követik céljukként. Ezzel nem értek egyet, mivel ez azt jelenti, hogy a teszteket ott kezdik alkalmazni, ahol nincs rájuk szükség. Tegyük fel például, hogy vannak szabványos CRUD (create/get/update/delete) műveletek a szolgáltatásunkban további logika nélkül. Ezek a metódusok pusztán közvetítők, amelyek a tárhellyel dolgozó rétegre ruházzák át a munkát. Ebben a helyzetben nincs mit tesztelnünk, kivéve talán azt, hogy az adott metódus hív-e DAO metódust, de ez vicc. A tesztek lefedettségének felmérésére általában további eszközöket használnak: JaCoCo, Cobertura, Clover, Emma stb. A téma részletesebb tanulmányozása érdekében A TDD a tesztvezérelt fejlesztés rövidítése. Ebben a megközelítésben, mielőtt bármi mást tenne, írjon egy tesztet, amely ellenőrzi az adott kódot. Ez fekete doboz tesztelésnek bizonyul: tudjuk, hogy a bemenet van, és tudjuk, hogy mi legyen a kimenet. Ez lehetővé teszi a kódduplikáció elkerülését. A tesztvezérelt fejlesztés az alkalmazás minden egyes funkciójához tesztek tervezésével és fejlesztésével kezdődik. A TDD megközelítésben először egy tesztet hozunk létre, amely meghatározza és teszteli a kód viselkedését. A TDD fő célja, hogy a kódot érthetőbbé, egyszerűbbé és hibamentessé tegye. Mindent az egységtesztről: technikák, fogalmak, gyakorlat - 3A megközelítés a következőkből áll:
  • Megírjuk a tesztünket.
  • Futtatjuk a tesztet. Nem meglepő módon meghiúsul, mivel még nem valósítottuk meg a szükséges logikát.
  • Adja hozzá a kódot, amely a teszt sikerességét okozza (újra futtatjuk a tesztet).
  • Refaktoráljuk a kódot.
A TDD egységteszteken alapul, mivel ezek a legkisebb építőelemek a tesztautomatizálási piramisban. Az egységtesztekkel bármely osztály üzleti logikáját tesztelhetjük. A BDD a viselkedésvezérelt fejlesztés rövidítése. Ez a megközelítés a TDD-n alapul. Pontosabban, egyszerű nyelvi példákat használ, amelyek elmagyarázzák a rendszer viselkedését a fejlesztésben résztvevők számára. Nem fogunk belemenni ebbe a kifejezésbe, mivel ez elsősorban a tesztelőket és az üzleti elemzőket érinti. A teszteset egy olyan forgatókönyv, amely leírja a tesztelendő kód ellenőrzéséhez szükséges lépéseket, konkrét feltételeket és paramétereket. A tesztberendezés olyan kód, amely beállítja a tesztkörnyezetet, hogy a tesztelt módszer sikeres futtatásához szükséges állapotú legyen. Ez objektumok előre meghatározott halmaza és viselkedésük meghatározott feltételek mellett.

A tesztelés szakaszai

A teszt három szakaszból áll:
  • Adja meg a tesztadatokat (fixtures).
  • Gyakorolja a tesztelés alatt álló kódot (hívja a tesztelt módszert).
  • Ellenőrizze az eredményeket, és hasonlítsa össze a várt eredményekkel.
Mindent az egységtesztről: technikák, fogalmak, gyakorlat - 4A tesztelési modularitás biztosítása érdekében el kell különíteni az alkalmazás más rétegeitől. Ez megtehető csonkok, gúnyok és kémek segítségével. A mockok olyan objektumok, amelyek testreszabhatók (például minden teszthez testreszabhatók). Lehetővé teszik, hogy megadjuk, mit várunk el a metódushívásoktól, azaz a várható válaszokat. Hamis objektumokat használunk annak ellenőrzésére, hogy azt kaptuk-e, amit várunk. A csonkok kemény kódolt választ adnak a hívásokra a tesztelés során. Ezenkívül információkat tárolhatnak a hívásról (például paramétereket vagy a hívások számát). Ezeket néha kémeknek is nevezik. Néha az emberek összekeverik a csonk és a gúny kifejezéseket: a különbség az, hogy a csonk nem ellenőriz semmit – csak szimulál egy adott állapotot. A gúny olyan tárgy, amelynek elvárásai vannak. Például, hogy egy adott metódust bizonyos számú alkalommal meg kell hívni. Más szavakkal,

Tesztkörnyezetek

Szóval, most a lényegre. Számos tesztkörnyezet (keretrendszer) érhető el a Java számára. Ezek közül a legnépszerűbb a JUnit és a TestNG. Az itt végzett áttekintésünkhöz a következőket használjuk: Mindent az egységtesztről: technikák, fogalmak, gyakorlat - 5A JUnit teszt egy olyan metódus egy osztályban, amelyet csak tesztelésre használnak. Az osztály neve általában ugyanaz, mint a tesztelt osztály, a végéhez fűzve a „Test” szót. Például CarService -> CarServiceTest. A Maven build rendszer automatikusan bevonja az ilyen osztályokat a tesztkörbe. Valójában ezt az osztályt tesztosztálynak nevezik. Nézzük röviden az alapvető megjegyzéseket:

  • A @Test azt jelzi, hogy a módszer egy teszt (alapvetően az ezzel a megjegyzéssel jelölt módszer egységteszt).
  • A @Before egy olyan metódust jelöl, amelyet minden teszt előtt végrehajtanak. Például egy osztály feltöltésére tesztadatokkal, bemeneti adatok olvasására stb.
  • Az @After az egyes tesztek után meghívott metódusok megjelölésére szolgál (pl. adatok törlésére vagy alapértelmezett értékek visszaállítására).
  • A @BeforeClass a @Before-hoz hasonló metódus fölé kerül. De egy ilyen metódust csak egyszer hívják meg az adott osztály összes tesztje előtt, ezért statikusnak kell lennie. Erőforrás-igényesebb műveletek végrehajtására szolgál, mint például egy tesztadatbázis felpörgetése.
  • Az @AfterClass a @BeforeClass ellentéte: az adott osztálynál egyszer fut le, de csak az összes teszt után. Például állandó erőforrások törlésére vagy az adatbázisról való leválasztásra használható.
  • Az @Ignore azt jelzi, hogy egy módszer le van tiltva, és figyelmen kívül hagyja a teljes tesztfutás során. Ezt különféle helyzetekben használják, például ha az alapmódszert megváltoztatták, és a tesztet még nem dolgozták át a változásokhoz. Ilyen esetekben célszerű leírást is hozzáadni, azaz @Ignore("Néhány leírás").
  • A @Test(expected = Exception.class) negatív tesztekhez használatos. Ezek olyan tesztek, amelyek azt ellenőrzik, hogy a metódus hogyan viselkedik hiba esetén, vagyis a teszt azt várja, hogy a metódus valamiféle kivételt dobjon. Az ilyen módszert a @Test annotáció jelzi, de jelzi, hogy melyik hibát kell elkapni.
  • A @Test(timeout = 100) ellenőrzi, hogy a metódus végrehajtása legfeljebb 100 ezredmásodperc alatt történik-e.
  • A @Mock a mező felett egy álobjektum hozzárendelésére szolgál (ez nem JUnit annotáció, hanem a Mockitotól származik). Szükség szerint közvetlenül a tesztmódszerben állítjuk be a modell viselkedését egy adott szituációhoz.
  • A @RunWith(MockitoJUnitRunner.class) egy osztály fölé kerül. Ez a megjegyzés arra utasítja a JUnit-et, hogy hívja meg a teszteket az osztályban. Különféle futók léteznek, köztük ezek: MockitoJUnitRunner, JUnitPlatform és SpringRunner. A JUnit 5-ben a @RunWith annotációt az erősebb @ExtendWith annotáció váltotta fel.
Nézzünk meg néhány módszert az eredmények összehasonlítására:

  • assertEquals(Object expects, Object facts) — ellenőrzi, hogy az átadott objektumok egyenlőek-e.
  • assertTrue(boolean flag) — ellenőrzi, hogy az átadott érték igaz-e.
  • assertFalse(boolean flag) — ellenőrzi, hogy az átadott érték hamis-e.
  • assertNull(Object object) — ellenőrzi, hogy az átadott objektum null-e.
  • assertSame(Object firstObject, Object secondObject) — ellenőrzi, hogy az átadott értékek ugyanarra az objektumra vonatkoznak-e.
  • állítsd, hogy(T t, Matcher matcher) — Ellenőrzi, hogy t megfelel-e a matcherben megadott feltételnek.
Az AssertJ egy hasznos összehasonlítási módszert is biztosít: assertThat(firstObject).isEqualTo(secondObject) . Itt említettem az alapvető módszereket – a többi a fentiek változatai.

Tesztelés a gyakorlatban

Most nézzük meg a fenti anyagot egy konkrét példán. Teszteljük egy szolgáltatás frissítési módszerét. Nem vesszük figyelembe a DAO réteget, mivel az alapértelmezettet használjuk. Adjunk hozzá egy indítót a tesztekhez:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <version>2.2.2.RELEASE</version>
   <scope>test</scope>
</dependency>
És itt van a szolgáltatási osztály:

@Service
@RequiredArgsConstructor
public class RobotServiceImpl implements RobotService {
   private final RobotDAO robotDAO;

   @Override
   public Robot update(Long id, Robot robot) {
       Robot found = robotDAO.findById(id);
       return robotDAO.update(Robot.builder()
               .id(id)
               .name(robot.getName() != null ? robot.getName() : found.getName())
               .cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
               .producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
               .build());
   }
}
8. sor – húzza ki a frissített objektumot az adatbázisból. 9-14. sorok – objektum létrehozása az építtetőn keresztül. Ha a bejövő objektumnak van mezője, állítsa be. Ha nem, akkor hagyjuk, ami az adatbázisban van. Most nézze meg tesztünket:

@RunWith(MockitoJUnitRunner.class)
public class RobotServiceImplTest {
   @Mock
   private RobotDAO robotDAO;

   private RobotServiceImpl robotService;

   private static Robot testRobot;

   @BeforeClass
   public static void prepareTestData() {
       testRobot = Robot
               .builder()
               .id(123L)
               .name("testRobotMolly")
               .cpu("Intel Core i7-9700K")
               .producer("China")
               .build();
   }

   @Before
   public void init() {
       robotService = new RobotServiceImpl(robotDAO);
   }
1. sor – a futónk. 4. sor – elszigeteljük a szolgáltatást a DAO rétegtől egy ál helyett. 11. sor – beállítunk egy tesztentitást (azt, amelyet tengerimalacként fogunk használni) az osztályhoz. 22. sor – beállítjuk a szolgáltatásobjektumot, amit tesztelni fogunk.

@Test
public void updateTest() {
   when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
   when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
   Robot robotForUpdate = Robot
           .builder()
           .name("Vally")
           .cpu("AMD Ryzen 7 2700X")
           .build();

   Robot resultRobot = robotService.update(123L, robotForUpdate);

   assertNotNull(resultRobot);
   assertSame(resultRobot.getId(),testRobot.getId());
   assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
   assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
   assertEquals(resultRobot.getProducer(),testRobot.getProducer());
}
Itt azt látjuk, hogy a tesztnek három egyértelmű felosztása van: 3-9. sorok – rögzítőelemek megadása. 11. sor – a tesztelés alatt lévő kód végrehajtása. 13-17. sor – az eredmények ellenőrzése. Részletesebben: 3-4. sor – állítsa be a DAO modell viselkedését. 5. sor – állítsa be a frissítendő példányt a szabványunkon felül. 11. sor – használja a metódust, és vegye fel a kapott példányt. 13. sor – ellenőrizze, hogy nem nulla-e. 14. sor – hasonlítsa össze az eredmény azonosítóját és a megadott metódus argumentumokat. 15. sor – ellenőrizze, hogy a név frissült-e. 16. sor – lásd a CPU eredményét. 17. sor – nem adtuk meg ezt a mezőt a példányban, így ennek változatlannak kell maradnia. Itt ellenőrizzük ezt a feltételt. Futtassuk:Mindent az egységtesztről: technikák, fogalmak, gyakorlat - 6A teszt zöld! Megkönnyebbülten fellélegezhetünk :) Összefoglalva, a tesztelés javítja a kód minőségét, rugalmasabbá és megbízhatóbbá teszi a fejlesztési folyamatot. Képzelje el, mennyi erőfeszítést igényel a több száz osztályfájlt magában foglaló szoftver újratervezése. Ha mindegyik osztályhoz megírtuk az egységteszteket, akkor magabiztosan tudunk refaktorálni. És ami a legfontosabb, segít könnyen megtalálni a hibákat a fejlesztés során. Srácok és lányok, ma ennyi van. Nyomj egy lájkot és írj kommentet :)
Hozzászólások
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION