Tesztelés Java környezetben

A szoftverfejlesztés egyik szükséges – és ma már bizonyára közhellyé lényegülten sokszor elmondott – következménye az elkészült program tesztelése. Mindazonáltal még mindig számos olyan fejlesztési feladat, projekt végeredménye hagyja el a „szoftvergyárat”, ahol a fejlesztett termék nincs megfelelően letesztelve. A kisebb-nagyobb, részben „házon belüli”, jellemzően egyedi szoftverfejlesztési projektek során nehezebb egy koncepció mentén kialakítható tesztelési folyamat végigvezetése.

A programozók saját fejlesztéseik, részfeladataik végzése közben a programkód növekedésével – természetszerűleg – egyre nagyobb kihívással szembesülnek, ami az áttekinthetőséget illeti. A több ezer soros programokban ennek megfelelően hibák keletkezhetnek, de ez nem is lehet kérdés.

A kérdés az, hogyan lehet megtalálni a keletkezett hibákat. Jellemzően azoknál az egyedi szoftverfejlesztéseknél, ahol a fejlesztői teszten (amikor a fejlesztők saját maguk munkáját, a programrész működését ellenőrzik) átesett végtermék – további szervezett tesztelések nélkül – a megrendelőhöz kerül, ott derülnek ki a hibás működéssel kapcsolatos problémák. Ez igen könnyen visszaüthet a szállítóra, ha a megrendelő ügyfél számára ez jelentős kényelmetlenséget, plusz költséget okoz.

A tesztelés, mint folyamat beépítése a szoftverfejlesztési projektekbe azért is fontos, mert a fejlesztők a saját maguk által írt programkódjukat egyszerűen képtelenek más szemszögből nézni. Még a jól felépített koncepcióba is csúszhatnak hibák, ezért kritikus fontosságú, hogy az elkészült terméket egy különálló, de természetesen a funkcionális specifikációt ismerő tesztelő csoport tesztelje.
A tesztelésekkel kapcsolatban általánosan ismert követelmények a józan ész és a tapasztalatok alapján kialakult alapelvek köré csoportosulnak. Például:

  • Mindig tervezettnek és megismételhetőnek kell lennie.
  • Más (is) teszteljen, mint aki fejlesztett.
  • Az egyes komponensek tesztelésétől kell haladni a teljes rendszer tesztelése felé. Érdemes a komponenseket külön letesztelni összeállítás előtt.
  • A komponensek még értelmezhető kisebb egységeinek tesztelését érdemes meghagyni fejlesztői tesztként, mert ilyen mélységben csak a fejlesztők ismerik a kódot.
  • A funkcionális működés ellenőrzésén túl szükség van a fejlesztett termék integrációs tesztjére a működési környezetbe helyezéskor.
  • A funkcionalitás megfelelőségének ellenőrzésén felül a terhelhetőség (teljesítmény) és a stabil működés (fenntarthatóság) ellenőrzése is fontos.
A Java-alapú szoftverfejlesztési feladatok során a tesztelési fázisban természetesen ugyanezek az alapszabályok érvényesek.
A következőkben három tesztelési lépést, azok egy-egy lehetséges módszerét/eszközét vesszük sorra, amelyek Java környezetben írt programok tesztelését segíthetik:

Komponens (modul) tesztelés

A komponensek belső működésének ellenőrzése a strukturális tesztek fogalomkörébe tartozik. Mivel a fejlesztett alkalmazás belső struktúráját vizsgáljuk, ezért white-box teszteknek is nevezik őket (ellentétben a funkcionális tesztekkel, ahol fekete dobozként tekintünk az alkalmazásra).

Célszerűen a különálló modulok egy egységbe építését megelőzően használható módszer, így kiküszöbölendő sok, a nagyobb egységgé gyúrt rendszerben fellépő hibák keresésével járó vesződség.
A komponens-szintű tesztelés lényege, hogy a szoftver metódusait, azok működését tesztesetekben meghatározott tesztértékekkel ellenőrizzük. Az egyes modultesztek sikerességét a lefedettséggel szokás jellemezni.

A lefedettség a programsorok relatív számával (teszt által elért programkód sorainak az összes sorhoz viszonyított száma) vagy a meghívott függvények relatív számával (végrehajtott függvények számának és az összes függvény számának hányadosa) jellemzően százalékos formában adható meg. Ide tartozik még pl. az elágazások, vagy az összes lehetséges út lefutásának ellenőrzése is. A lefedettség mérése mind a memória, mind a processzor számára jelentős terhelést jelenthet, ezt a tesztek tervezésénél időzítés, terhelés szempontjából figyelembe kell venni. Másik vonatkozása a lefedettségmérésnek, hogy az adott projekt költségét jelentősen növelheti, ez pedig általában erős befolyásoló tényező.

Az ismétlődő végrehajtás, a tesztesetek számossága, a teljes vagy részleges végrehajtás szükségessége és az eredmények megjelenítése, rendszerezése, továbbá az automatizált végrehajtási igények kezelése és mindezek együttes bonyolultsága nélkülözhetetlenné teszi az ilyen feladatokra készített keretrendszerek használatát.

A JUnit (http://www.junit.org/) az egyik legelterjedtebb keretrendszer, amit az egyes komponensek tesztelésére használnak. Bár lefedettséget nem tud mérni (erre a célra kiegészítő eszközöket lehet alkalmazni, ilyenek: pl. Cobertura, Emma), ezzel együtt ez az egyik legnépszerűbb komponenstesztelő eszköz, amelyet a leggyakrabban használt fejlesztői környezetek (pl. Eclipse, NetBeans) is támogatnak. Egyszerű használhatósága mellett lehetőséget nyújt automatikus tesztfuttatásra a kód egészére, vagy egy részére vonatkozóan, továbbá a teszteredmények megjelenítése mellett strukturált riportok készítése is lehetséges.

A JUnit-ban hierarchikusan rendezett tesztesetekből állíthatunk össze tesztkészleteket (test suite), amelyekben a teszteseteket együtt futtathatjuk. A tesztek ellenőrzésére szolgálnak az ún. Assert-metódusok, amelyekkel a teszt eredményét hasonlítjuk össze az elvárt eredménnyel.

A metódusok tesztelésekor is figyelemmel kell lenni arra, hogy a metódusok ne önmagukban kerüljenek letesztelésre, hanem az üzleti funkciójukat lássák el (rendszerben gondolkodás).

A tesztek tervezésében többnyire az emberi képzelőerőre (vagy kiegészítő eszközre) van szükségünk, a JUnit önmagában nem támogat tervezést. A tesztesetek tervezése eléggé hosszadalmas feladat, ráadásul az a kihívás is előttünk áll, hogy megtaláljuk az arany középutat a tesztesetek szükséges és elégséges száma (lefedettség), valamint az aránytalanul nagy energiaráfordítás között.

Tesztesetek tervezésekor figyelembe kell vennünk az egyes változók szélsőértékeit, a beállítandó intervallumokat, érvénytelen karaktereket stb.

Az egyes modulok működésének tesztelését követően az integrációs tesztekkel célszerű ellenőrizni a modulok „valós” környezetbe illesztésekor a tranzakciós folyamatokat és a modulok egymásra hatását.

Funkcionális (rendszer) tesztelés

A funkcionális tesztekre akkor kerülhet sor, ha az összeállított alkalmazás egyes komponensei unit és modulszinten (modul alatt itt pl. egymásra ható unitokat értünk) megfelelően működnek.

A funkcionális tesztelés már jobban elkülöníthető a fejlesztőktől. Általános esetben egy különálló tesztcsapat feladata, hogy a tesztelésre kapott rendszer üzleti funkcionalitását figyelembe véve, de magát a megvalósítási módot fekete dobozként kezelve megvizsgálja az egyes funkciók működését. Ismerve a lehetséges bemeneteket, le kell ellenőrizni a lehetséges kimeneteket. A funkcionális tesztek egy-egy tesztesetre építve egy-egy felhasználói funkció, üzleti folyamat lépéseit ellenőrzik.

A funkcionális tesztek során alapvetően az alábbi típusú tesztek kerülnek végrehajtásra (természetesen egyes módszerek alkalmazhatók pl. a modulok tesztelésénél is):

Ekvivalencia osztályok alapján történő tesztelés

A program bemeneti paramétereit a specifikáció szerint felállított ún. ekvivalencia-osztályokra (azonos viselkedési jelleggel bíró értékek halmazaira; pl. ilyenek az intervallumok) osztjuk, majd minden osztályból tesztértékeket véve teszteseteket készítünk. A jól megválogatott tesztesetek (pl. érvényes-érvénytelen, határérték-középérték) így a lehetséges inputokat nagymértékben lefedik, így egy olyan halmazt állítunk elő a teszteléshez, amely a lehető legtöbb hibát felderítheti.

Határérték tesztelés

Tulajdonképpen az ekvivalencia osztályok alkalmazásának speciális esete, ugyanis az osztályok határértékeire fókuszálva, a hibák rendszerint itt sűrűsödnek. Határérték-tesztelésnél a kimeneti értékek vizsgálata is szükséges lehet, azaz érdemes a tesztekhez olyan bemeneti értékeket is választani, ami a kimeneti határértékeket fogja vizsgálni.

Ok-okozati tesztelés

A funkcionális tesztelés talán legfontosabb célja, hogy kiderüljön, milyen bemeneti paraméterek hatására mi történik a kimeneten. Ennek vizsgálatára egy ok-okozati gráfot vagy táblázatot célszerű készíteni, amit a tesztesetek kidolgozásakor használhatunk. Természetesen a lehetséges bemeneti-kimeneti kombinációk száma óriási lehet, ezért sokszor szinte képtelenség minden egyes esetet megvizsgálni. Ezért fontos azoknak a teszteset-kombinációknak az összeállítása, amelyekkel a leghatékonyabban tudjuk feltérképezni a hiányosságokat, illetve a specifikációnak ellentmondó hibákat.

Az így összeállított teszteseteket egy ún. tesztlefedési mátrixon megjelenítve feltérképezhetjük, hogy az adott teszt(eset) mely funkció(k) vizsgálatára alkalmas, valamint hogy maradt-e ellenőrzés nélküli funkció.

Véletlenszerű tesztelés

A véletlenszerűen előállított tesztadatokkal elvégzett vizsgálatok célja azoknak a hibáknak a feltárása, amelyek a determinisztikus tesztek tervezésénél esetleg elkerülték a figyelmet. A véletlenszerű tesztelés hibafeltáró képessége rendszerint alacsony, de általában megéri a ráfordítást, mert a tesztadatok (véletlen számok) viszonylag gyors előállítása újabb területen fedheti fel az eddig elrejtett hibákat.

A funkcionális tesztelés, a tesztesetek végrehajtása részben automatizálható, azonban néhány esetben szükségszerű lehet a manuális tesztelés is, mint kiegészítő módszer.

Az automatizálás történhet pl. scriptek segítségével parancssoros alkalmazásoknál, de akár egy vagy több különálló grafikus megjelenítésű teszteszközzel is, pl. webes alkalmazásoknál.

Miért hasznos a tesztelés automatizálása?

Függetlenül a tesztelés módszerétől, javítások után gyakori a regressziós tesztelés szükségessége, a tesztesetek akárhányszor futtathatók, valamint lerövidül a visszajelzések ideje a fejlesztők számára, így hatékonyabbá válik a fejlesztési folyamat (ez az előny felhasználható pl. az agilis szoftverfejlesztésnél vagy az extrém programozási technikánál).

Selenium

A webes alkalmazások automatikus tesztelésére használható eszközök egyike a Selenium (http://seleniumhq.org/), amellyel funkcionális teszteket is végre tudunk hajtani.

A Selenium segítségével a leggyakrabban használt webes böngészőkben futtathatunk egy-egy tesztesetet vagy egész tesztforgatókönyvet is úgy, mintha egy felhasználó futtatná őket. A Selenium többféle operációs rendszert is támogat, így felhasználhatósága széleskörűnek mondható a webes felületek tesztelésében.

A Selenium több komponensből áll, amelyek az automatizált tesztelés másfajta megközelítését szolgálják.

A fő komponensek az alábbiak:

  • Selenium IDE: Fejlesztői környezet, amellyel rögzíthetők a böngésző felületén elvégzett feladatok (kattintások), melyek aztán lefuttathatók, szerkeszthetők és hibakeresésre használhatók. Hátránya, hogy csak Firefox böngészőben futtatható. A teszteket futtató keretrendszer neve Selenium Core, ami az IDE és az RC motorja is egyben.

  • Selenium RC (Remote Controller): Szerver és kliens könyvtárak összetevőiből álló, többféle programozási nyelvet támogató API, melynek segítségével automatikusan – akár távoli gépen lévő böngészőben is – futtathatók az elkészített tesztesetek. (A Selenium WebDriver a legújabb verziójú Selenium egyik komponense. Az RC modulhoz hasonló funkcionalitással bíró API, amellyel az RC komponenst fogja kiváltani a jövőben.)
    A Selenium egy saját, egyszerű „nyelvvel” rendelkezik, amelyből könnyen összeállíthatók a tesztesetek és ezek más programozási nyelvre is elmenthetők a fejlesztői környezetből. A Selenium alapnyelvi összetevői az alkalmazások állapotának manipulálására szolgáló parancsok (command), az állapotok vizsgálatára és tárolására szolgáló ún. Accessor elemek és az alkalmazás állapotát az elvárt eredménnyel összevető Assertions elemek.
    A rögzített vagy átszerkesztett tesztesetek exportálhatók többféle formátumba (pl. Java, Perl, PHP) és más környezetben is futtathatók az API segítségével.

  • Selenium Grid: Ez a komponens párhuzamosan több szerveren futtatható tesztek elvégzését teszi lehetővé, így jelentős idő takarítható meg a webes tesztelések során, valamint egyszerre tesztelhető egy teljesen heterogén környezet.

Természetesen nem mindig éri meg a felülettesztek automatizálása: ha a határidő nagyon rövid vagy a felhasználói felület gyakran változik, akkor a tesztesetek újraírása hosszabb időt vehet igénybe, mint a manuális tesztelés.

A funkcionális tesztekhez sorolható a rendszerintegrációs tesztelés, ami az interfészeken kapcsolódó más rendszerek hatását is ellenőrző tesztfázis.

A funkcionális teszteken túl szükséges azokat a nem funkcionális teszteket is elvégezni, amelyek a működő rendszer használata közbeni viselkedést vizsgálják.

Terheléses tesztek

Amikor az elkészült alkalmazások funkcionálisan már működőképesek, végső működési környezetükbe kerülés előtt fontos ellenőrizni a rendszer teljesítőképességét. Ezt általában már a tesztelési folyamatot lezáró végső felhasználói teszt keretében végzik el. A tervezés során specifikált hardverigény, a tervezett párhuzamos felhasználók száma, a rendszer adott időszakban történő elérhetősége és válaszideje mind olyan követelmények, amelyeket az elkészült alkalmazásnak teljesítenie kell. Ezeknek a feltételeknek a vizsgálatára szolgálnak a terheléses tesztek.

A terheléses tesztekben többféle módszerrel vizsgálhatjuk az elkészült rendszert. Vizsgálhatjuk a kívánt paraméterek szerinti teljesítményt, vagy akár a rendszer túlterheltségének hatását, a huzamosabb ideig tartó csúcsterhelés hatását, alkalmazhatjuk a fokozatos terhelés módszerét stb.

A terheléses teszteknél különösen célszerű ügyelni arra, hogy a tesztrendszer paraméterei összemérhetők legyenek az éles rendszer paramétereivel. Ezzel biztosíthatjuk, hogy a tesztrendszerben mért teljesítmény-értékek az éles rendszerben is mérvadóak legyenek.

A webes alkalmazások (de akár fájlszerver kapcsolatok, adatbázis kapcsolatok, Java objektumok, web szolgáltatások stb.) teljesítménytesztelésére használható egyik eszköz a több operációs rendszeren is működő JMeter (http://jakarta.apache.org/jmeter/). A JMeter-ben egy grafikus felület segítségével lehet a teszteseteket összeállítani, a tesztek eredményeit táblázatokban, grafikonokon tudjuk megtekinteni. Egy beépített proxy-szerver segítségével a küldött üzenetek rögzíthetők és újra lejátszhatók.

A JMeter több szálon futó tesztelést is képes végrehajtani az ún. Thread Group-ok segítségével, így szimulálva az egyes felhasználókat, akik különböző oldalakat próbálnak elérni. Az időzítők segítségével pedig a terhelés elosztását is szabályozni lehet.

A JMeter-rel elvégzendő tesztek tervezésekor használatos elemek a tesztet vezérlő elemek: az ún. Sampler (ami a szerver felé történő kommunikációt indítja és a választ várja) és a logikai vezérlő (logika annak eldöntésére, hogy a kérés milyen feltétellel, mikor induljon).

A szervertől visszaérkező válaszok elvárt eredménnyel való összehasonlítására használt elem az ún. Assertion elem. A Listener-ek feladata pedig az adatok összegyűjtése és az igények szerinti riportokhoz szükséges adatok előállítása.

Ezekből a JMeter-ben használatos alapelemekből építhető fel a különféle szerverek teljesítménytesztjéhez szükséges tesztterv. A részletesebb beállítások természetesen már a teszt céljától és tárgyától függően mások lesznek, ám a JMeter működésének megértéséhez ezek az elemek nélkülözhetetlenek.

Végezetül megemlítendő még a tesztelés, mint a szoftverfejlesztési életciklus egyik fontos szakasza, ami a projekt fejlesztési költségeinek jelentős részét teheti ki. A tesztek egy része automatizálható, ahogy a fenti példák is mutatják, persze a tesztesetek felépítése itt sem fogja csökkenteni a ráfordítást. Ám a jól megírt tesztek hatékonyabbá tehetik a tesztelést magát, ráadásul az ismétlődő tesztek már egyszerűbben elvégezhetők a bejáratott úton, ami összességében csökkentheti a projekt ráfordítását.

Szerző:
Csáki István

<< Vissza