ugrás a tartalomhoz

PHP osztályok egységtesztelése

erenon · 2010. Ápr. 1. (Cs), 13.23
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.

A sorozatban megjelent



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
A parancs kimenetként valami hasonlót kell kapnunk – a verzió természetesen változhat:
PHPUnit 3.5.0 by Sebastian Bergmann.
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:

<?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;
    }
}
Aki valami zavart érez az osztállyal kapcsolatban, az helyesen teszi. Ezután készítsük el az osztályunkat tesztelő osztályt:

<?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)
        );
    }
}
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:
$ phpunit arithmetic-test.php
Kimenetként valami hasonlót várunk:
PHPUnit 3.5.0 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 2.75Mb

OK (1 test, 1 assertion)
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.

<?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)
        );
    }
}
Figyeljük meg a változásokat: Létrehoztunk egy 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

Az Arithmetic 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);  
    }
Tesztjeink 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:
    
    /**
     * 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;
    }
Valamint készítsünk egy MathError.php fájlt az Exception mappában, a következő tartalommal:

<?php
class Exception_MathError extends Exception
{}
Megjegyzés: A 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.
 
1

örülök a cikknek

winston · 2010. Ápr. 1. (Cs), 16.08
nagyon örülök a cikknek, magyar viszonylatokban "hiánycikk" az egységtesztelés használata, de még az ismerete is: nehéz olyan (php) fejlesztőt találni, aki egyátalán hallott róla és fent sem mindig könnyű elfogadtatni, hogy van értelme (már ha egyátalán van idő :)). én ugyan (főleg otthon) használok egységtesztelést, de ennek ellenére köszönöm a cikket, jól jönnek az ilyen cikkek, talán növelik a magyar phpfejlesztői kultúrát.
2

Bezony

Pozo · 2010. Ápr. 1. (Cs), 16.12
+1 kávé mellé jólesett ;)
3

Idő

Ifju · 2010. Ápr. 2. (P), 09.09
+1 örvendezés a cikknek

fent sem mindig könnyű elfogadtatni, hogy van értelme (már ha egyátalán van 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.
4

egyetértek

winston · 2010. Ápr. 2. (P), 09.30
én személy szerint egyetértek veled, de ezek nem mind rajtam múlnak :)
5

Félreértés ne essék: nem

Ifju · 2010. Ápr. 2. (P), 11.28
Félreértés ne essék: nem személyeskedni akartam, csak a gondolatra reagáltam. :)
6

nem vettem magamra

winston · 2010. Ápr. 2. (P), 11.46
nem vettem magamra, csak gondoltam ne legyen félreértés :)
7

nagyon jo

imehesz · 2010. Ápr. 5. (H), 18.15
hali,

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
8

Múltkorában olvastam egy

nathefellow · 2010. Júl. 1. (Cs), 19.37
Múltkorában olvastam egy Zendframework-ös könyvet és abban is volt szó erről a PHPUnit-ról, akkor nagyon megtetszett és azóta is használom/próbálgatom.
9

TDD hogyan?

kemmma · 2012. Jan. 6. (P), 07.12
Sziasztok, olvasgatom ezt a cikket is, más cikket is, de egyszerűen nem tudom megfogni ezt az egészet. Remélem, hogy lesz itt majd valaki, aki segít helyére tenni a dolgokat. :)

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éle set, get függvénye meg egy render. 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 a return $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...
10

Tesztelés

MadBence · 2012. Jan. 6. (P), 14.46
Senki sem mondta, hogy a tesztelés könnyű :). Jó teszteket írni meg egyenesen művészet (sokkal nehezebb jó tesztet írni, mint jó programot!)
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)
11

Re: Tesztelés

kemmma · 2012. Jan. 7. (Szo), 11.32
Hát... nem mondom, hogy okosabb lettem. De még rágom a témát...
12

Pont ugyanez a problémám

deejayy · 2012. Jan. 7. (Szo), 18.17
Pont ugyanez a problémám nekem is, de az első kérdésedre tudom a választ. Tehát ha lefeded az összes általad ismert hibalehetőséget egy teszttel: ha komplex rendszer építesz, akkor a dolgok egymásra hatnak. Ha valami olyat módosítasz, ami a(z általad említett) 9 ponton valamit úgy módosít, hogy hibás lesz az eredmény, máris kiütközik.

É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?)
13

Azt hiszem rossz felé

pp · 2012. Jan. 8. (V), 09.31
Azt hiszem rossz felé indultok el akkor amikor a hibák számát végesnek tekintitek, nem beszélve arról, hogy inkább ne is beszéljünk hibáról, vagy definiáljuk máshogyan azt.

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
14

Köszönet

kemmma · 2012. Jan. 10. (K), 11.50
Szia, azt hiszem, hogy az itt leírtak új megvilágításba tették ezt az egészet.

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.