Ebben a leckében a programozással kapcsolatos, az eddigi lépéseknél kevésbé kreatív, ámde nem nélkülözhető gyakorlati ismeretek közül a teszteléssel ismerkedünk meg.
A tesztelés célja, hogy minél több hibát megtaláljunk a programban. Ahhoz, hogy az összes hibát fölfedezzük, kézenfekvőnek tűnik a programot kipróbálni az összes lehetséges bemenő adattal. Ez azonban sajnos nem lehetséges. Nézzünk egy egyszerű példát:
Pszeudokódos algoritmus | Struktogram |
---|---|
Program: Változó I,J:Egész Be: I,J Ki: I,J Program vége. |
|
A struktogramban – általában – nem jelennek meg az adatok deklarációi, ez kivétel. A külső dobozrészben az algoritmus neve – általában – az eljárás, ill. függvény fejsora, azaz neve és paraméterezése.
Mivel 216 különböző értékű egész számot tudunk tárolni, ezért az összes lehetőség 232, aminek a leírásához már 9 számjegyre van szükség. Ez rengeteg időt venne igénybe, így nem is járható út. A másik végletet a következő – két szám osztására (!) készült – meglepő programrészleten mutatjuk be:
Pszeudokódos algoritmus | Struktogram |
---|---|
Program: Változó A,B:Egész Be: A,B Ki: A*B [A/B kiírása] Program vége. |
Ha ezt a programot olyan bemenő adatokkal próbáljuk ki, amelyben A=0 vagy B=1, akkor a program helyesen működik, a hibát nem tudjuk felfedezni. Ezután azt gondolhatnánk, hogy reménytelen helyzetbe kerültünk: hiszen minden lehetséges adattal nem tudjuk kipróbálni a programot; ha pedig kevesebbel próbáljuk ki, akkor lehet, hogy nem vesszük észre a hibákat. A helyzet azért nem ennyire rossz: célunk csak az lehet, hogy a tesztelést olyan módszerrel hajtsuk végre, amellyel a próbák száma erősen lecsökkenthető.
Tesztesetnek a be- és kimeneti adatok és feltételek együttes megadását nevezzük. Akkor tudunk a tesztelés eredményeiről bármit is mondani, ha van elképzelésünk arról, hogy adott bemenő adatra milyen eredményt várunk.
Fogalmazzuk meg a tesztelés alapelveit:
A programtesztelés módszereit két csoportba oszthatjuk aszerint, hogy a tesztelés során végrehajtjuk-e a programot, vagy nem. Ha csak a program kódját vizsgáljuk, akkor statikus, ha a programot végre is hajtjuk a tesztelés során, akkor dinamikus tesztelésről beszélünk.
A kódellenőrzés a program szövegének megvizsgálását jelenti. Az algoritmus logikáját kell ekkor a programban végigkövetni, s megfigyelni, hogy a kettő eltér-e egymástól. Csupán a kód alapján is viszonylag könnyen tud hibákat felfedezni egy avatatlan, a program készítőjétől különböző személy. Sokszor szerencsés, ha a program készítője elmondja valakinek – soronként –, hogy mit csinál a program. Ilyenkor gyakran saját maga fedezi fel a hibákat. Ez általában is igaz: a helyes megértés leghatásosabb próbája az új ismeretek továbbadása, megmagyarázása. Eközben derülhet fény a gondolatsor, a program rosszul, felületesen értelmezett vagy kidolgozatlan részeire.
Egy programban az előforduló hibákat két csoportra oszthatjuk: formai (szintaktikai), illetve tartalmi (szemantikai) jellegű hibákra. Ha értelmezőt használunk, akkor a formai ellenőrzést megfelelően választott tesztadatokkal való kipróbálással lehet elérni. A program minden utasítását végre kell hajtani legalább egyszer.
Igen sok információt szolgáltat egy programról, ha különböző kereszthivatkozás-táblázatokat készítünk róla (keresztreferencia). Ennek egyik típusa a változókról készült táblázat, melynek oszlopai a következőket tartalmazhatják: adatnév, típus és a hivatkozási helyek pontosítása.
A hivatkozási helyeket azonosító lista lehet egy sorszámlista (vagy eljárásazonosító és azon belüli relatív sorszám), amiben azok a sorok sorszámai szerepelnek, amelyekben az adott változó előfordul. A sorszám mögött speciális jellel (pl. *-gal) jelölhetjük, hogy a változó értéket kapott az adott sorban.
Az alábbi példában előfordul egy G globális változó és egy ALFA a Forgat eljáráson belül definiált, lokális adat.
adatnév | típus | hivatkozási helyek azonosítása |
---|---|---|
Forgat.ALFA | Valós | Forgat 19*, 22, 25 |
G | Egész | 29, 53* |
Természetesen kézzel készíteni ilyen táblázatot nagyon munkaigényes feladat, sok esetben azonban a fordítóprogram biztosítja automatikusan ezt a lehetőséget.
A formai hibáknál sokkal nehezebb felfedni a programban a tartalmi hibákat. Ezek formailag helyes programokban fordulnak elő, de mégis felismerhetők formális eszközökkel. Ezek általában önmagukban értelmetlenek, a programba valószínűleg elírás, figyelmetlenség miatt kerültek bele (de korántsem biztos, hogy tényleg hibásak).
Az egyes programegységek deklarációs részében szereplő konstansok, típusok, változók, eljárások, függvények, operátorok, más modulokra hivatkozások a programegység törzsében szerepelnek-e.
Ez a vizsgálat a beágyazott programegységekre is kiterjesztendő.
Ilyen hiba lehet az, hogy egy változónak értéket adunk, de ezután nem használjuk semmire, vagy közvetlenül utána még egyszer értéket kap.
Az alábbi példában valószínűleg a fölösleges I:=1 értékadás egy korábbi amíg-os ciklus javítása után maradhatott benn:
Pszeudokódos algoritmus | Struktogram |
---|---|
I:=1 Ciklus I=1-től 5-ig ... Ciklus vége |
Az ellenkező véglet, amikor egy változó, amelyiknek nincs kezdőértéke, előbb szerepel kifejezésben, mint értékadás bal oldalán.
Ez a fajta hiba nehezen vehető észre mindig formálisan, a program (legalább szimbolikus) végrehajtása nélkül.
Példa erre a következő programrészlet:
Pszeudokódos algoritmus | Struktogram |
---|---|
Ha f akkor I:=1 különben J:=1 K:=I |
Ha egy változó önmagának ad értéket, esetleg néhány hatástalan érték kombinálásával, az vagy felesleges, vagy pedig egy esetleges elírásra utalhat.
I:=1*I-0 [talán az I:=I*I-O a helyes, vagy I:=I*I-I0?]
Van:=Van≠Hamis
Itt és a továbbiakban, ha a pszeudokódos és a struktogramos algoritmus minimálisan tér el, csak a pszeudokódost adjuk meg, és nem zárjuk dobozba a műveleteket.
Az elágazás- és ciklusfeltételek formailag helyesek lehetnek úgy is, hogy a bennük szereplő változók értékétől függetlenül mindig azonos értéket vehetnek fel. Ekkor az elágazás egyik ága biztosan nem hajtható végre, a ciklus pedig vagy végtelen lesz, vagy pedig a ciklusmagot egyszer sem hajtjuk végre.
Egy-két példa ilyen feltételekre: I<1 és I≥100, I<I+1
A logikai feltételekhez hasonlóan az aritmetikai kifejezéseknél is gyanús a konstansság, amikor a kifejezés vagy egy részének értéke nem függ a benne szereplő változóktól. Ez egyes esetekben viszonylagos bonyolultságuk miatt nehezen észrevehető lehet:
X:=A2-B2-(A+B)*(A-B)
Y:=cos(X)/sin(X)*tan(x)
Vagy egy kevésbé nyilvánvaló példa:
X:=A3/((A-B)*(A-C))+B3/((B-C)*(B-A))+C3/((C-A)*(C-B))
ami egyszerűsítve mindössze:
X:=A+B+C
Ennek, illetve az előzőnek a felismeréséhez kifejezések algebrai egyszerűsítőjével kell rendelkeznünk! (Ez egyébként hatékonyságjavítóként is működhet, jó fordítóprogramokban van ilyen optimalizáló eszköz.)
Az eddigiektől eltérő végtelen ciklust okozhat, ha egy számlálós ciklusban – amennyiben a nyelv szintaxisa egyáltalán megengedi – megváltoztatjuk a ciklusváltozót:
Pszeudokódos algoritmus | Struktogram |
---|---|
Ciklus I=1-től N-ig ... I:=1 Ciklus vége A ciklusbeli értékadás talán I:=I-1 vagy I:-1 ? |
Szintén végtelen ciklusra utal, ha egy feltételes ciklusnál a ciklusfeltételben szereplő változók a ciklusmagban nem változhatnak meg (mert pl. nem szerepelnek értékadás bal oldalán). Sajnos a programozási nyelvek megengedik, hogy a ciklusváltozót a ciklusmagban hívott eljárás is módosítsa, emiatt a biztos felismerése nagyon nehéz.
Akkor fordul elő, ha függvény- vagy operátordefiníció elágazást tartalmaz, és az elágazás valamelyik ágán nem adunk vissza függvényértéket.
Pszeudokódos algoritmus | Struktogram |
---|---|
Függvény Absz(X):Egész Ha X<0 akkor Absz:=-X Függvény vége. |
Matematikai értelemben a függvények a függvényérték meghatározásán kívül mást nem csinálhatnak. A programozási nyelvek többsége azonban megengedi, hogy a függvények megváltoztassák paramétereik értékét, vagy akár globális változókat is.
Ebből sok probléma származhat, például az Y:=f(X)+f(X) és az Y:=2*f(X) kifejezés értéke különböző is lehet!.
Az előző részben a statikus tesztelési módszereket vizsgáltuk, ahol a tesztelést a program végrehajtása nélkül, a program szövegének vizsgálatával végeztük. A dinamikus tesztelési módszerek alapelve éppen az, hogy a programot működés közben vizsgáljuk.
Teszteseteket kétféle módon tudunk választani. Egy lehetőség az ún. feketedoboz-módszer, más néven adatvezérelt tesztelés. E módszer alkalmazásakor a tesztelő nem veszi figyelembe a program belső szerkezetét, pontosabban nem azt tekinti elsődleges szempontnak, hanem a teszteseteket a feladat meghatározás alapján választja meg.
A cél természetesen a lehető leghatékonyabb tesztelés elvégzése, azaz az összes hiba megtalálása a programban. Ez ugyan elvileg lehetséges, kimerítő bemenet tesztelést kell végrehajtani, a programot ki kell próbálni az összes lehetséges bemenő adatra. Ezzel a módszerrel azonban, mint korábban láttuk, mennyiségi akadályba ütközhetünk.
Egy másik lehetőség a fehérdoboz-módszer (logika vezérelt tesztelés). Ebben a módszerben a tesztesetek megválasztásánál lehetőség van a program belső szerkezetének figyelembevételére is.
A cél a program minél alaposabb tesztelése, erre jó módszer a kimerítő út tesztelés. Ez azt jelenti, hogy a programban az összes lehetséges utat végigjárjuk, azaz annyi tesztesetet hozunk létre, hogy ezt elérhessük vele. Az a probléma, hogy még viszonylag kis programok esetén is igen nagy lehet a tesztelési utak száma. Gondoljunk a ciklusokra! Sőt ezzel a módszerrel a hiányzó utakat nem lehet felderíteni.
Mivel sem a fehérdoboz-módszerrel, sem a feketedoboz-módszerrel nem lehetséges a kimerítő tesztelés, el kell fogadnunk, hogy nem tudjuk egyetlen program hibamentességét sem szavatolni. A további cél ezek után az összes lehetséges teszteset halmazából a lehető leghatékonyabb teszteset-csoport kiválasztása lehet.
A tesztelés hatékonyságát kétféle jellemző határozza meg: a tesztelés költsége és a felfedett hibák aránya. A leghatékonyabb teszteset-csoport tehát minimális költséggel maximális számú hibát fed fel.
Definíció: Próbának nevezzük a tesztesetek azon halmazát, amellyel a programot teszteléskor kipróbáljuk.
Definíció: Ideális próba az a próba, amellyel a programban szereplő összes hibajelenség felfedezhető.
Definíció: Adott e szinten megbízható próba az a próba, amellyel 1–e valószínűséggel felfedjük az összes hibajelenséget.
A megbízhatóság pontos mérése sem oldható meg általában, így a teszteléskor csak megérzéseinkre alapozhatunk.
A tesztelés alapelveinek ismertetésénél jó tesztesetnek neveztük az olyant, amelyre minél nagyobb valószínűséggel áll, hogy hibát találunk vele a programban. Másrészt megállapítottuk, hogy a kimerítő bemeneti tesztelés gyakorlatilag megvalósíthatatlan, így meg kell elégednünk a bemenő adatok egy szűk részhalmazának tesztelésével. Ezek után azért, hogy ez a részhalmaz minél hatásosabb legyen, a benne szereplő tesztesetekre teljesüljenek a következők:
Ezeket az elveket veszi figyelembe az ekvivalenciaosztályok módszere. Ekvivalenciaosztályokat nemcsak az érvényes, hanem az érvénytelen adatokhoz is létre kell hozni, és a programot azokkal is kipróbálni.
Néhány jó tanács az ekvivalenciaosztályok megtalálásához:
Számoljuk meg egy maximum 40 karakteres szöveg magánhangzóit!
Az ekvivalenciaosztályok:
Ez utóbbi osztályt esetleg újabb osztályokra bonthatjuk:
Lehetséges azonban egy másik osztályokra bontás is:
Ha már rendelkezésünkre állnak az ekvivalenciaosztályok, akkor a teszteseteket a következő két elv alapján határozhatjuk meg:
A határeset-elemzés két dologban különbözik az ekvivalenciaosztályok keresésének módszerétől; annak bizonyos szempontból kiegészítő módszere:
Felsorolunk néhány szempontot a határeset-elemzés megvalósításának elősegítéséhez:
Számoljuk meg egy maximum 40 karakteres szöveg magánhangzóit!
Bemenet szerint három próba kell: 0, 40, 41 karakteres szöveg.
Kimenet szerint két próba kell: nincs magánhangzó / minden betű magánhangzó.
Az ilyen a módszereknél a tesztelést a programszöveg ismeretében végezzük. A tesztelés három lépés egymásutánjából áll:
Itt azt kell meghatároznunk, hogy a programgráfban szereplő mely utak mentén kell kipróbálni a programot.
Utasítások egyszeri lefedésének elve
A módszer neve arra utal, hogy a programgráf csomópontjait kell lefedni. A módszer lényege olyan tesztesetek kiválasztása, amelyek alapján minden utasítást legalább egyszer végrehajthatunk a programban. Bár ez sokszor jó módszer, de nem tökéletes; nézzük meg ugyanis a következő egyszerű példát: Ha X>0 akkor Ki: X
Ebben a példában egyetlen próbával elérhetjük az összes utasítás végrehajtását (pl. X=1), de nem derülne ki az, ha az X>0 feltétel helyett az X≥0 szerepelne, azaz a program hibás lenne.
Döntéslefedés elve
A módszer neve arra utal, hogy a programgráf éleit kell lefedni. Itt az előzőnél egy kicsit erősebb követelményt alkalmazunk. A programban minden egyes elágazás igaz, illetve hamis ágát legalább egyszer be kell járni a tesztelés során. A döntéslefedés elvét figyelembe véve eleget teszünk az utasításlefedés követelményének is.
Itt is maradnak azonban problémák, nézzünk egy példát: Ha X>0 és Y>0 akkor Ki: X*Y
Ebben az esetben az (X=1, Y=1) és az (X=-1, Y=-1) tesztesetek lefedik a döntéseket, de nem vennénk észre velük azt, ha a második feltételt (Y>0) rosszul írtuk (vagy lehagytuk) volna.
A részfeltétel-lefedés elve
Ebben az esetben olyan teszteseteket kell készíteni, amelyhez a döntésekben szereplő minden részfeltételt legalább egyszer HAMIS, illetve IGAZ eredménnyel értékelünk ki.
Az előbbi példát eszerint ki kell próbálni a következő tesztesetekkel: (X=1,Y=1), (X=-1, Y=-1), (X=1, Y=-1)
A speciális tesztesetek elve
Az előbbi elvek mellett – a feketedoboz-módszereknél ismertetett ötletek alapján – válasszunk az egyes utakhoz speciális teszteseteket is!
Az előző példát még próbáljuk ki például az (X=0, Y=-1) és az (X=1, Y=0) tesztesetekkel is!
Hogyan lehet eldönteni, hogy mely adatokkal próbáljuk ki a programot ahhoz, hogy a választott kipróbálási stratégiát jól alkalmazzuk?
A legegyszerűbb eset az, hogy véletlenszerűen választunk adatokat, és a program végrehajtása során vizsgáljuk, hogy mely utakon jártunk. A pontos leíráshoz egy-két definícióra van szükségünk, amelyek a program gráffal való leírására épülnek.
Definíció: Bázisútnak nevezzük a programgráf olyan útját, amely
E definíció az élek egyszeres lefedéséhez illeszkedik, a csomópontok egyszeres lefedéséhez el kell hagyni az olyan bázisutakat, amelyek részei más bázisútnak.
Definíció: A vezérél a bázisút első éle.
Most már megoldhatjuk az előbb felvetett problémát. Helyezzünk el a program minden vezérélén egy-egy számlálót! Próbáljuk ki a programot újabb és újabb tesztadatokkal mindaddig, amíg mindegyik számláló értéke legalább 1 nem lesz!
Alapvető probléma, hogy ez a módszer nagyon gazdaságtalan, hiszen semmilyen segítséget nem ad a tesztadatok választásához, és így esetleg csak nagyon sok kipróbálással tudjuk megvalósítani a kipróbálási stratégiát. A jobb megoldáshoz újabb definíciókat nézzünk.
Definíció: Tesztutaknak nevezzük a programgráfon átvezető, a kezdőponttól a végpontig haladó olyan utakat, amelyek minden bennük szereplő élt pontosan egyszer tartalmaznak.
Definíció: Tesztpredikátumnak nevezzük azokat a bemenő adatokra vonatkozó feltételeket, amelyek teljesülése esetén pontosan egy tesztúton kell végighaladni.
A teszteset-generálás első lépése tehát a minimális számú olyan tesztút meghatározása, amelyek lefedik a kipróbálási stratégiának megfelelően a programgráfot.
Ha ezzel készen vagyunk, akkor határozzuk meg ezen tesztutak tesztpredikátumait! Ehhez a program szimbolikus végrehajtására van szükség. Induljunk ki az előfeltételből! Haladjunk a programban az első elágazás- vagy ciklusfeltételig, és a formulát a közbülső műveleteknek megfelelően transzformáljuk! A tesztútnak megfelelő ág feltételét és kapcsolattal kapcsoljuk hozzá a tesztpredikátumhoz, majd folytassuk a szimbolikus végrehajtást egészen a program végpontjáig!
Probléma lehet ezzel az, hogy lehetetlen tesztutat választottunk, azaz a kapott tesztpredikátum azonosan hamis lesz.
Harmadik lépésként minden egyes tesztpredikátumhoz (amelyek egy-egy ekvivalenciaosztályt reprezentálnak) válasszunk egy-egy tesztesetet!
Egy osztály tanulói átlagának alapján a jelesek számának meghatározása.
Pszeudokódos algoritmus | Struktogram |
---|---|
Program: Be: N,ÁT DB:=0; I:=1 Ciklus amíg I≤N ← 1 Ha ÁT[I]>4.5 akkor DB:=DB+1 ← 2 I:=I+1 Ciklus vége Ki: DB Program vége. |
A programban két feltételkiértékelés szerepel. A programon átvezető tesztutak:
E három útból minimálisként az utolsó kettőt választjuk, az első mindegyiküknek része.
Az előfeltétel: N≥0 és ∀j(1≤j≤N): 1≤ÁT[j]≤5
Mindkét tesztútnál el kell jutni először a ciklusig, emiatt tesztpredikátumuk így alakul:
B-C. N≥0 és ∀j(1≤j≤N): 1≤ÁT[j]≤5 és DB=0 és I=1
Vegyük most figyelembe, hogy a ciklusfeltételnek először igaznak kell lennie:
B-C. N≥0 és ∀j(1≤j≤N): 1≤ÁT[j]≤5 és DB=0 és I=1 és I≤N
Ebből következik:
B-C. N≥ 1 és ∀j(1≤j≤N): 1≤ÁT[j]≤5 és DB=0 és I=1
Következik az elágazásfeltétel, amelyet a két tesztútnál kétféleképpen kell figyelembe venni:
B. N≥1 és ∀j(1≤j≤N): 1≤ÁT[j]≤5 és DB=0 és I=1 és ÁT[I]>4.5
C. N≥1 és ∀j(1≤j≤N): 1≤ÁT[j]≤5 és DB=0 és I=1 és ÁT[I]≤4.5
Az első esetben végre kell hajtani az elágazásban szereplő növelést, a másodikban nem, utánuk a tesztpredikátum egy egyszerű átalakítással így alakul:
B. N≥1 és ∀j(1≤j≤N): 1≤ÁT[j]≤5 és DB=1 és I=1 és ÁT[1]>4.5
C. N≥1 és ∀j(1≤j≤N): 1≤ÁT[j]≤5 és DB=0 és I=1 és ÁT[1]≤4.5
A ciklusváltozó növelése után a ciklusfeltételnek már nem szabad teljesülnie:
B. N≥1 és ∀j(1≤j≤N): 1≤ÁT[j]≤5 és DB=1 és I=2 és ÁT[1]>4.5 és I>N
C. N≥1 és ∀j(1≤j≤N): 1≤ÁT[j]≤5 és DB=0 és I=2 és ÁT[1]≤4.5 és I>N
Ebből újabb transzformációval kapjuk a végleges tesztpredikátumokat (a bemenetben nem szereplő változók elhagyásával):
B. N=1 és ∀j(1≤j≤N): 1≤ÁT[j]≤5 és ÁT[1]>4.5
C. N=1 és ∀j(1≤j≤N): 1≤ÁT[j]≤5 és ÁT[1]≤4.5
Maradt még a konkrét tesztesetek meghatározása:
B. (N=1,ÁT[1]=5)
C. (N=1,ÁT[1]=4)
Van a tesztelésnek néhány speciális változata, amikor nem a teljes (vagy parciális helyesség) belátása a cél. Vizsgáljuk sorra ezeket!
Annak vizsgálata, hogy a specifikáció ellentmondásmentes-e, tartalmaz-e hiányosságokat, ...
Szerepel-e a programban az összes megvalósítandó funkció? Menürendszerű programban a megjelenő, választható funkciók megvannak-e? A kívánt eredmények mindegyike megjelenik-e? ...
A program ellenőrzi-e a felhasználótól kapott adatokat? Hibás adatokkal meg lehet-e zavarni működését? Ellenőrzi-e a szükséges perifériák, file-ok létét, típusát, a file-ok elhelyezkedését a háttértáron?
A program számára az adatok nagyon nagy sebességgel érkeznek, egyszerre többfelől is érkeznek, ...: mi történik ilyenkor? Megfelelő hatékonysággal működik-e ekkor a program?
A programnak nagyon sok adatot kell kezelnie (az előfeltételben megadott maximális számút). Helyesen működik-e ekkor is a program? Megfelelő hatékonysággal működik-e ekkor a program?
A program végrehajtási idejének (átlagos, minimális, maximális), valamint változói helyfoglalásának meghatározása tesztesetekkel, a javítás és az eredeti összevetése e szempontból.
Önálló modulokból álló programrendszer moduljainak egyenkénti tesztelése, a többi modultól függetlenül.
Önálló modulokból álló programrendszer moduljai összeépítésének ellenőrzése.
Az animáció bemutatja a tesztesetek kiválasztásának alapjait: