PHP osztályok egységtesztelése
Minden megírt kódsor után előveszed a böngészőt, hogy megnézd, működik-e a megírt kód? Előfordult már, hogy egy függvényt többféle adattal is tesztelned kellett, minden egyes módosítás után? Mindig ki kellett töltened a 15 elemű űrlapodat, hogy megnézd, működik-e a rekord rögzítése? Ezután mindig manuálisan törölni kellett a teszt adatokat? Ha ezek alapján ráismersz az általános munkastílusodra, itt az idő, hogy megismerkedj az egységteszteléssel. Egy módszer, ami segít jobb minőségű kód előállításában, a hibakeresésben és refaktorizálásban, lecsökkentve a tesztelésre fordított időt.
Ha egy tesztelési folyamatot egynél többször kell végrehajtanunk kézzel, hamar belátjuk, egy olyan feladatot végzünk újra és újra, amit egy gép is meg tudna csinálni helyettünk, gyorsabban, olcsóbban és jobban. Bízzuk hát számítógépünkre a tesztelés feladatát automatizált tesztek segítségével. Ha egy fejlesztés alatt álló komponens tesztelése folyamán figyelembe kell vennünk más komponenseket is, tesztjeink megbízhatatlanok és túlzottan összetettek lesznek. Az egységtesztelés szellemében megírt kódkomponensek tesztelhetőek más komponensek nélkül, így nem csak nagyobb megbízhatóságot, hanem könnyebb újrahasznosíthatóságot elérve.
Az egységtesztelés folyamán ilyen komponenseket (egységeket) tesztelünk előre meghatározott adatokkal és környezettel, más egység bevonása nélkül; méghozzá a kód írása előtt, közben és után, folyamatosan.
A telepítéshez általában root jogosultság szükséges, Ubuntu rendszereken írjuk aA parancs kimenetként valami hasonlót kell kapnunk – a verzió természetesen változhat:A PHPUnit egy keretrendszert biztosít számunkra, mely megkönnyíti az egységtesztelés folyamatát. Lehetővé teszi tesztdublőrök egyszerű létrehozását, melyek imitálják a tesztelt komponens függőségeinek viselkedését, továbbá jegyzi a végrehajtott teszteket és jelentést készít azokról. Ezen kívűl több hasznos funkcióval is rendelkezik, melyek csak részben érintik az egységtesztelés feladatkörét.
Készítsük el első osztályunkat, melyet majd tesztek alá kívánunk vetni. Valósítson meg matematikai alapműveleteket, például az osztást:Aki valami zavart érez az osztállyal kapcsolatban, az helyesen teszi. Ezután készítsük el az osztályunkat tesztelő osztályt:A két fájlt helyezzük el egy közös mappába, majd a terminálból a mappában állva adjuk ki a következő parancsot:Kimenetként valami hasonlót várunk:Ami itt fontos, az az árva pont a fejléc és a statisztikák között. Minden egyes tesztet egy karakter reprezentál a kimenetben, mely a lefutástól függően lehet egy pont siker, vagy F betű sikertelenség esetén, ezentúl felvehet más értékeket is. Az osztályunk átment a teszten, de könnyen beláthatjuk, hogy egy teszt nem teszt. Módosítsuk úgy a teszt osztályunkat, hogy különböző értékekkel tesztelje a kiszemelt metódust.Figyeljük meg a változásokat: Létrehoztunk egy
Tegyük fel, hogy 0-val osztás esetén aTesztjeink lefuttatása után a 3 pont mellett egy E betűt is kapunk, mellékelve a hibaüzenettel: Division by zero.
Megjegyzés: Tesztünk nem azért bukott meg, mert nem dobott kivételt, hanem mert hiba lépett fel a futás során, amit a phpunit elkapott. Ezért látunk E betűt (Error) F (Fail) helyett.
Miután látjuk a sikertelen tesztet, javítsuk ki a tesztelt metódust:Valamint készítsünk egy Megjegyzés: A
Tesztjeinket lefuttatva már 4 pontot kapunk, jelezve a 4 sikeres tesztet. Ezt a módszert, amikor a hiba javítását egy teszt mentén végezzük, test-driven bug fixingnek nevezzük.
Kiegészítés: Amennyiben nem tudjuk azonnal megoldani a problémát, jegyezzünk be egy új hibát a hibakezelő rendszerünkben. Ezután helyezzük el a
Könnyen beláthatjuk, hogy nagy méretű rendszerek esetén egy jól megírt tesztgyűjtemény jelentősen megkönnyíti a fejlesztést, bár sok időbe telik a megírása, és kiterjedt követelményeket, valamint az osztályok felületének pontos definícióját igényli. Ilyen mértékű megtervezése komolyabb méretű rendszereknek egyébként is szükséges, így ezek megléte nem lehet akadály. A befektetett idő pedig bőven megtérül a hibakeresés, hibajavítás és refaktorizálás során, nem beszélve arról, hogy a program írása során a sikertelen teszteket látva mindig tudjuk, merre tovább.
Ilyen technikákról (TDD), a tesztek ajánlott szervezéséről, a PHPUnit konfigurációjáról és egységteszt-barát kódról lesz szó a cikk következő részében.
■ A sorozatban megjelent
- PHP osztályok egységtesztelése
- PHP osztályok egységtesztelése II
Ha egy tesztelési folyamatot egynél többször kell végrehajtanunk kézzel, hamar belátjuk, egy olyan feladatot végzünk újra és újra, amit egy gép is meg tudna csinálni helyettünk, gyorsabban, olcsóbban és jobban. Bízzuk hát számítógépünkre a tesztelés feladatát automatizált tesztek segítségével. Ha egy fejlesztés alatt álló komponens tesztelése folyamán figyelembe kell vennünk más komponenseket is, tesztjeink megbízhatatlanok és túlzottan összetettek lesznek. Az egységtesztelés szellemében megírt kódkomponensek tesztelhetőek más komponensek nélkül, így nem csak nagyobb megbízhatóságot, hanem könnyebb újrahasznosíthatóságot elérve.
Az egységtesztelés folyamán ilyen komponenseket (egységeket) tesztelünk előre meghatározott adatokkal és környezettel, más egység bevonása nélkül; méghozzá a kód írása előtt, közben és után, folyamatosan.
Egységtesztelés PHP nyelven
Az egységtesztelés megvalósítására több eszköz is rendelkezésünkre áll, ezek közül a PHPUnit nevűvel fogunk most ismerkedni. A telepítése Linux rendszereken a következőképpen történik:$ pear channel-discover pear.phpunit.de
$ pear channel-discover pear.symfony-project.com
$ pear install phpunit/PHPUnit
A telepítéshez általában root jogosultság szükséges, Ubuntu rendszereken írjuk a
sudo
parancsot a pear
elé. További telepítési lehetőségekről a PHPUnit manual megfelelő részében olvashatunk. Miután végeztünk, gépeljük a következő parancsot a terminálunkba:$ phpunit --version
PHPUnit 3.5.0 by Sebastian Bergmann.
Készítsük el első osztályunkat, melyet majd tesztek alá kívánunk vetni. Valósítson meg matematikai alapműveleteket, például az osztást:
<?php
/**
* arithmetic.php
*/
/**
* Egyszerű matematikai alapműveleteket megvalósító osztály
*/
class Arithmetic
{
/**
* Osztás
*
* @param int $a Osztandó
* @param int $b Osztó
* @return int $a és $b hányadosa
*/
public function division($a, $b)
{
return $a/$b;
}
}
<?php
/*
* arithmetic-test.php
*/
require_once 'arithmetic.php';
/**
* Az Arithmetic osztály műveleteit tesztelő metódusok osztálya
*
* Figyeljük meg, hogy tesztelő osztályunk rendelkezik szülőosztállyal:
* A PHPUnit_Framework_TestCase segítségével a phpunit által futtathatóvá válik osztályunk,
* valamint assert funkciókat is kapunk, melyek a konkrét teszteket végzik.
*/
class ArithmeticTest extends PHPUnit_Framework_TestCase
{
/**
* Az osztás műveletet tesztelő metódus
*/
public function testDivision()
{
$a = 10;
$b = 5;
$return = 2;
$arithmetic = new Arithmetic();
/*
* az assertEquals metódust a PHPUnit_Framework_TestCase osztály definiálja,
* összehasonlítja a két paraméterét, és egyenlőség esetén igazat ad vissza.
* A phpunit az ilyen assert metódusok kimenetéről fog a phpunit jelentést készíteni
*/
$this->assertEquals(
$return,
$arithmetic->division($a, $b)
);
}
}
$ phpunit arithmetic-test.php
PHPUnit 3.5.0 by Sebastian Bergmann.
.
Time: 0 seconds, Memory: 2.75Mb
OK (1 test, 1 assertion)
<?php
/*
* arithmetic-test.php
*/
require_once 'arithmetic.php';
/**
* Az Arithmetic osztály műveleteit tesztelő metódusok osztálya
*/
class ArithmeticTest extends PHPUnit_Framework_TestCase
{
/**
* A testDivision függvénynek szolgáltat teszt adatokat
*
* @see testDivision
*/
public function divisionProvider()
{
return array(
array(10, 5, 2),
array(6, 2, 3),
array(9, 3, 3)
);
}
/**
* Az osztás műveletet tesztelő metódus
*
* @param int $a Osztandó
* @param int $b Osztó
* @return int $a és $b hányadosa
*
* @dataProvider divisionProvider
*/
public function testDivision($a, $b, $return)
{
$arithmetic = new Arithmetic();
$this->assertEquals(
$return,
$arithmetic->division($a, $b)
);
}
}
divisionProvider
metódust, ami a teszt adatokat szolgáltatja egy tömbben. A tömb minden egyes eleme egy újabb tömb. A PHPUnit minden egyes tömb esetén meghívja a testDivision
függvényt, átadva a tömb elemeit paraméterként. Így az ebben az esetben háromszor fog lefutni. A testDivision
függvény paramétereit módosítottuk, hogy fogadni tudja a teszt értékeket, és dokumentációjában feltüntettük a @dataProvider
annotációt. Csupán ez a dokumentációs bővítés szükséges a feladat végrehajtásához, de praktikus ennél sokkal bővebben dokumentálni a metódusainkat. A teszt futtatása esetén 3 pontot kell látnunk az eredmény mezőben.Hibajavítás teszteléssel
AzArithmetic
osztályt használva hamar rájöhetünk, hogy valami nincs rendben vele a tesztek ellenére. Ha $b
0-nak adódik, hiba lép fel (0-val osztás). Ez nyilvánvalóan hiba, amit javítanunk kell. Mielőtt azonban nekiesnénk a kódunknak, írjunk egy tesztet. A teszteset most lefuttatva legyen sikertelen, a javítás után pedig sikeres. Így megbizonyosodhatunk arról, hogy a hibajavításunk az igényeknek megfelelő.Tegyük fel, hogy 0-val osztás esetén a
division
metódusnak egy Exception_MathError
kivételt kell dobnia. Íme a teszteset, ami ezt ellenőrzi:
/**
* Ellenőrzi a 0-val osztás esetén dobandó kivételt
*
* @expectedException Exception_MathError
*/
public function testDivisionByNull()
{
$arithmetic = new Arithmetic();
$arithmetic->division(1, 0);
}
Megjegyzés: Tesztünk nem azért bukott meg, mert nem dobott kivételt, hanem mert hiba lépett fel a futás során, amit a phpunit elkapott. Ezért látunk E betűt (Error) F (Fail) helyett.
Miután látjuk a sikertelen tesztet, javítsuk ki a tesztelt metódust:
/**
* Osztás
*
* @param int $a Osztandó
* @param int $b Osztó
* @return int $a és $b hányadosa
* @throws Exception_MathError 0-val osztás esetén
*/
public function division($a, $b)
{
if ($b == 0) {
require_once 'Exception/MathError.php';
throw new Exception_MathError();
}
return $a/$b;
}
MathError.php
fájlt az Exception
mappában, a következő tartalommal:
<?php
class Exception_MathError extends Exception
{}
division
függvényünk dokumentációjában feltüntettük a @throws
annotációt, figyelmeztetve az esetleges kivételre. Ez nem szükséges a teszteléshez, de ennek ellenére minden esetben illik feltüntetni, a jobb átláthatóság érdekében.Tesztjeinket lefuttatva már 4 pontot kapunk, jelezve a 4 sikeres tesztet. Ezt a módszert, amikor a hiba javítását egy teszt mentén végezzük, test-driven bug fixingnek nevezzük.
Kiegészítés: Amennyiben nem tudjuk azonnal megoldani a problémát, jegyezzünk be egy új hibát a hibakezelő rendszerünkben. Ezután helyezzük el a
@ticket
annotációt a sikertelen teszt dokumentációjában, például @ticket 35
. Ha a teszt sikeres, a PHPUnit lezárja a 35-ös számú hibát, ha sikertelen, újra megnyitja azt, amennyiben előzőleg lezárásra került. A PHPUnit jelenleg a Trac és GitHub hibakezelő rendszereket támogatja, a funkció igénybevételéhez további konfiguráció szükséges.Könnyen beláthatjuk, hogy nagy méretű rendszerek esetén egy jól megírt tesztgyűjtemény jelentősen megkönnyíti a fejlesztést, bár sok időbe telik a megírása, és kiterjedt követelményeket, valamint az osztályok felületének pontos definícióját igényli. Ilyen mértékű megtervezése komolyabb méretű rendszereknek egyébként is szükséges, így ezek megléte nem lehet akadály. A befektetett idő pedig bőven megtérül a hibakeresés, hibajavítás és refaktorizálás során, nem beszélve arról, hogy a program írása során a sikertelen teszteket látva mindig tudjuk, merre tovább.
Ilyen technikákról (TDD), a tesztek ajánlott szervezéséről, a PHPUnit konfigurációjáról és egységteszt-barát kódról lesz szó a cikk következő részében.
örülök a cikknek
Bezony
Idő
Nekem a fejlesztési rutinom része, hogy írom és futtatom a teszteket folyamatosan, mert rengeteg hiba még azelőtt kiderül, hogy egyáltalán integrálnám az adott kódrészletet a meglévő projektbe, és azon belül tesztelném.
Összességében szerintem inkább időt takarít meg, miután az ember ráérzett az ízére.
egyetértek
Félreértés ne essék: nem
nem vettem magamra
nagyon jo
Nagyon jo, hogy mar vegre otthon is kezd beindulni ez a fajta hozzaallas.
Egyszer lattam egy (web) fejlesztesi technikat (Ruby on Rails -> TDD alapu) es ott a bongeszot kb. 15 max 20%-ban hasznaltak a programozok. Nagyon bejott.
Azt is fontos megemliteni, hogy a jobb keretrendszerek (Rails, ZendFramework, Yii Framework stb.) mar alapbol kinaljak ezt a lehetoseget.
--iM
Múltkorában olvastam egy
TDD hogyan?
Nem a TDD-t szeretném megkérdőjelezni, túl sok általam nagyrabecsült programozó hívta fel a figyelmet a TDD hasznosságára, így e hozzászólást mindenképpen a válaszok keresése íratta velem! :)
A cikkben rendkívül zavart egy mondat, illetve ez számomra megkérdőjelezte az egész értékét: "Aki valami zavart érez az osztállyal kapcsolatban, az helyesen teszi."
Nem! Nem azért szeretnék tesztet készíteni, mert tudom, hogy hibás a kód, hanem azért mert szeretném az összes lehetséges hibát elkerülni. Vagy nem is jó oldalról közelítem meg a dolgot? (ez számomra egy rendkívül fontos kérdés!)
Adott egy feladat, van benne tíz buktató, ismerek ebből kilencet, elkészítem hozzájuk a megfelelő teszteket, majd büszkén kipipálom ezt a szakaszt, hogy letesztelve. Pedig nem, csak én hiszem azt. Ekkor mi van?
Amúgy gyakorlatban sem tudom megfogni, többféle problémám is van, az egyik, hogy van egy absztrakt osztályom, nem tudom példányosítani, hogy teszteljem? Ha egy új osztályt készítek, akkor már azzal hibát követhetek el, nem? Schrödinger macskája :) Bár az sem egészen tiszta, hogy a privát elemek vizsgálatával mi a helyzet?
A következő problémám, hogy a triviális dolgokat hogyan teszteljük, adott egy
HtmlPage
nevű osztály, nevéhez hűen HTML oldalakat állítunk elő, (egyszerűség kedvéért) van mindenféleset
,get
függvénye meg egyrender
. Hogy tesztelem? Beállítok mindenféle értéket, megnézem hogy a render ugyanazt a kimenetet adja, mint amit előre elkészítek?És mi van, ha az is hibás? Mert lusta voltam kézzel legyártani a teszt output-ot, és már azt is a hibás osztállyal írtam.
Vagy mi van akkor, ha átállunk HTML 4-ről HTML 5-re? A tesztek nem működnek majd, pedig csak az egyik privát változó értéke lett más. Pedig én csak azt szeretném, hogy az apró módosításaimat biztosra tudjam, hogy nem okoz gondot.
Harmadik problémám, az adatbázisban lévő adatokkal kapcsolatban van, adott egy osztály, benne egy
getValamiById
függvény, ami leegyszerűsítve areturn $this->db->getRow("SELECT * FROM valami WHERE id = ".$id);
utasítás. Ekkor mit ellenőrzök? Teszt adatbázisban teszt adatokkal vizsgálom? Vagy éles adatokkal, de mi van, ha a megadott sor már megváltozott, vagy nem is létezik? Vagy másoljam ki az osztályból a lekérést és azzal teszteljem? De mi van, ha a lekérés csak egyszerűnek néz ki, de mégsem az... ám én balga módon kimásoltam és az egyik hibás lekérést hasonlítottam össze a másik hibás lekéréssel...Ilyen és ehhez hasonló kérdések miatt egyszerűen nem tudom, hogy miként álljak hozzá ehhez az egészhez...
Tesztelés
A TDD-t az motiválja, hogy még amikor egy sor kódot sem írtál le, már tudod, hogy mi lesz az elvárt eredmény. (Mondjuk ha a te elvárt eredményed "hibás", akkor az egésznek nem sok értelme van)
A kódban így sem fogsz kevesebb hibát véteni, csak ezek gyorsabban kiderülnek (ezzel időt spórolsz). Az olyan hibák, amik nem jönnek elő teszt közben, a jóisten sem fogja megtalálni neked. (viszont lehet viszonylag pontosan becsülni egy programban az összes hiba számát)
Az utolsó kérdésedre szerintem ott a válasz a második részében a cikknek.
(az absztrakt osztályos problémádat pedig nem értem. éles rendszeren sem fogsz absztrakt osztályt példányosítani, azaz nem fog hibát okozni. A leszármazott, példányosítható osztályain viszont minden további nélkül le tudod tesztelni a szülő funkcionalitását is)
Re: Tesztelés
Pont ugyanez a problémám
És persze mondjuk a 10. hibalehetőség - amit nem ismersz - majd debuggal kijön. A TDD nem hibátlan kódot, hanem kevesebb hibát illetve a "mik az új fícsörök - a bugok" esetet próbálja redukálni. De ha kidebuggolod a 10. lehetőséget is, akkor írsz rá egy tesztet és máris lefedted.
Jómagam egyébként egyelőre a napló alapú kódolást űzöm: minden függvényre akasztottam egy logot, mindent előforduló hibát egyből fájlba irányítok, sőt, a sikeres műveleteket is figyelem (és nem csak hiba esetén, hanem folyamatosan). Tehát úgy érzem haladok a TDD felé, de még csak szemmel értékelem ki a teszteket :) (ennek egyébként van valami elnevezése?)
Azt hiszem rossz felé
A TDD arról szól, hogy van egy helyem, ahol definiálva van, hogy milyen bemeneti értékekre milyen kimeneteket várok. Tehát a nem definiált értékekre, nem definiálok kimenetet, ergo azt nem tekintem hibának.
Ez a módszer nem a hibamentes kód írásában segít, hanem a specifikációnak megfelelő kód írását teszi lehetővé.
Képzeld el azt, hogy egy folyamatosan változó kódot tartasz karban. Folyamatosan jönnek a felhasználói igények és aszerint változtatod a kódodat. Eljön az idő amikor már bizonyos kódrészletek tarthatatlanok lesznek, átírásra szorulnak. Ez a refaktor, amikor azért írjuk át a kódot, hogy könnyebb legyen a fejlesztés, de fontos, hogy ugyan úgy működjön(tehát jobb nem lesz, csak könnyebben karbantartható). Honnan fogod tudni, hogy ugyan úgy működik? Ilyenkor átgondolod, hogy hol okozhat hibát a módosításod és azt teszteled? Biztos jól gondoltad át? Biztos, hogy csak ott okozott problémát ahol gondoltad?
Legtöbben akik végiggondolják ezeket a kérdéseket inkább letesznek a refaktorról. Vannak azok, akik nem gondolják végig, csak nekiállnak. Az első komoly bukta után rettegni fognak tőle: „Ha egy kód jól működik akkor ahhoz nem nyúlunk” Ismerős?
A probléma ott van, hogy eljutnak ahhoz az igencsak nagy stresszt okozó kérdéshez, hogy hogyan fogom tudni ellenőrizni, hogy jól dolgoztam-e. A tesztek erre adnak választ. Azt tekintem jónak ami megfelel a teszteknek.
Igen, ha kevés, vagy rossz tesztem van akkor ez nem segít, de a teszt az nem egy valami ami különáll a program többi részétől. A teszt ugyan úgy a kód része. A teszt képes elkapni a tesztben lévő hibákat is. (pl. ha amit tesztelek az jól működik. :D) A tesztet ugyan úgy karban kell tartanom, refaktorálnom kell mint a kód többi részét.
Csak akkor látod meg ennek az értelmét, ha csapatban dolgozol és folyamatosan változó igényeknek megfelelő kódot kell írnod. Márpedig az igények változnak. Megteheted, hogy rinyálsz, hogy „az ügyfél így, meg úgy”, meg „miért nem lehet megmondani előre mi kell”, de megteheted azt is, hogy elfogadod, hogy ez egy olyan igény amit ki kell elégítened és ehhez keresel eszközöket. (a TDD csak egy ezek közül)
pp
Köszönet
Tehát nem a hibákon, hibakeresésen van most a hangsúly, hanem a specifikáción, mármint a TDD abban segít, hogy a kódom megfeleljen a specifikációnak.