Přejít k navigační liště

Zdroják » PHP » Testování v PHP: Instalace a základy PHPUnit

Testování v PHP: Instalace a základy PHPUnit

Články PHP, Různé

V tomto seriálu se podrobně seznámíme s problematikou testování kódu v PHP, a to od úplných začátků po pokročilé metody testování integrace, mockování a další. Druhou částí seriálu bude průvodce tvorbou testovatelného kódu v PHP.

minulém díle jsme se seznámili s nejnutnějšími teoretickými základy testování, dnes se podíváme na základy používání testovacího frameworku PHPUnit.

Instalace

Pokud již máte PHPUnit nainstalován, pak můžete tuto kapitolu přeskočit a přejít hned k základům jeho používání. PHPUnit je možné nainstalovat hned několika způsoby, z nichž nejjednodušší je asi pomocí PEARu. Jako první je dobré si aktualizovat samotný PEAR. Podotýkám, že všechny aktualizace nebo instalace je nutné provádět jako root.

pear channel-update pear.php.net
pear upgrade-all

Poté nastavit správné kanály.

pear channel-discover pear.phpunit.de
pear channel-discover components.ez.no
pear channel-discover pear.symfony-project.com
pear update-channels

Nyní už můžeme nainstalovat samotný PHPUnit.

pear install --alldeps --force phpunit/PHPUnit

Instalace zabere jen chvilku a po jejím dokončení by nám příkaz:

phpunit --version

měl vrátit (dle nainstalované verze) výstup podobný:

PHPUnit 3.6.11 by Sebastian Bergmann

Další možnosti instalace

Pokud nemůžete nebo prostě jen nechcete instalovat PHPUnit pomocí PEARu, pak můžete samozřejmě využít svůj balíčkovací systém:

yum install php-phpunit-PHPUnit
apt-get install phpunit

V neposlední řadě je PHPUnit možné jednoduše stáhnout pomocí Gitu:

https://github.com/sebastianbergmann/phpunit/#using-phpunit-from-a-git-checkout

Ani u jednoho „alternativního“ způsobu jsem nenarazil na žádný zásadní problém (jak se občas můžete dočíst). Pokud byste přece jen na nějaký narazili, napište do diskuse pod článkem.

Základy PHPUnit

Framework máme nainstalovaný, pojďme se vrhnout na nějaký první test. Aby byl první příklad alespoň trochu smysluplný, musíme mít co testovat. Proto si vytvoříme jednoduchou třídu Calc, která poskytuje dvě metody: multiply pro násobení a divide pro dělení. Implementace obou metod nechme pro ilustraci prosté, bez ošetření vstupních hodnot.

class Calc {

    public function multiply($x, $y)
    {
        return ($x * $y);
    }

    public function divide($x, $y)
    {
        return ($x / $y);
    }
}

Testy v PHPUnit jsou samostatné třídy, které dědí (ve většině případů) od PHPUnit_Framework_TestCase. Tyto třídy jsou jakýmisi kontejnery na jednotlivé testovací případy, což jsou metody, jejichž názvy začínají prefixem test. Tento prefix zajistí, že PHPUnit takto pojmenované metody sám zavolá. Pokud z nějakého důvodu nechceme tento prefix použít, pak máme ještě možnost testovací metodu označit anotací @test. Možnosti anotací si ukážeme v příštím díle.

Test case vs. TestCase

Možná jste zaznamenali drobný nesoulad ve významu pojmu „test case“, který by měl spíše označovat každou jednotlivou metodu, tedy testovací případ, a nikoli celou sadu. Na toto si bohužel budete muset zvyknout, jde patrně o historický problém ve jmenné konvenci. Pojmem „test case“ budu v tomto seriálu označovat konkrétní testovací případ, tedy metodu. Třídu dědící od PHPUnit_Framework_TestCase a obsahující testovací případy budu označovat jako sadu testů.

Dobrým zvykem je testovací třídy pojmenovávat s postfixem Test a ukládat do souborů, jejichž názvy také nesou tento postfix a jsou umístěny v analogické struktuře jako zdrojové soubory. První umožní aby PHPUnit sám vyhledal a načetl všechny testy, druhé pak pomůže s udržením přehledu a pořádku v testech. Viz náčrt níže.

 src
 |-- lib
 |-- |-- Calc.php
 |-- |-- Math.php
 test
 |-- lib
 |-- |-- CalcTest.php
 |-- |-- MathTest.php

První test

class CalcTest extends PHPUnit_Framework_TestCase {

    private $calc;

    protected function setUp()
    {
        $this->calc = new Calc();
    }

    public function testMultiply()
    {
        $this->assertEquals(10, $this->calc->multiply(2, 5), "Chyba nasobeni: 2 x 5 != 10");
    }

    public function testDivide()
    {
        $this->assertEquals(2, $this->calc->divide(10, 5), "Chyba deleni: 10 / 5 != 2");
    }

}

První jednoduchá sada testů je na světě. Jak už jsem uváděl výše – název třídy nese postfix Test a dědí od PHPUnit_Framework_TestCase. Naše sada obsahuje dva testovací případy: jeden pro metodu multiply, jeden pro metodu divide. V obou případech používáme asertační metodu assertEquals. Omlouvám se za tento trochu kostrbatý překlad, ale neznám vhodný český ekvivalent. Metoda assertEquals očekává dva povinné parametry: očekávaná hodnota a vypočtená hodnota. Pokud se obě hodnoty rovnají, pak je test vyhodnocen jako pravdivý, v opačném případě jako neplatný. Jako třetí, nepovinný, parametr je možné uvést textovou zprávu, která bude zobrazena na výstup, dojde-li k selhání testu. U pravdivých testů je tato zpráva ignorována.

Spouštění testů

Spuštění testu je snadné, stačí spustit příkaz phpunit s cestou k adresáři s testy jako parametrem. Pokud se nejprve do tohoto adresáře přesune, pak stačí: phpunit . (včetně tečky). Jako výstup můžeme očekávat:

$ phpunit .
PHPUnit 3.6.11 by Sebastian Bergmann.

..

Time: 0 seconds, Memory: 3.25Mb

OK (2 tests, 2 assertions)

Tečky v řádku pod číslem verze označují pravdivé (splněné) test cases. Dojde-li k selhání nějakého test case, pak je místo tečky uvedeno jedno z písmen:

  • F = fail. Takto je označen test case, který selhal. S největší pravděpodobností nebyl splněn předpoklad v nějaké asertační metodě. Tedy nesoulad mezi předpokládanou a skutečnou návratovou hodnotou.
  • E = error. Toto není selhání testu, ale runtime chyba. Může jít o nezachycenou výjimku, nebo některou z odchytitelných chyb v php (notice, warning, error). PHPUnit interně převádí všechny tyto chyby na výjimky.
  • I = incompleted. Takto jsou označeny test cases, kterým jsme sami nastavili příznak nedokončeného. Toto je možné provést zavoláním metody markTestIncomplete($msg) s jedním nepovinným parametrem – zprávou (důvod označení). Po jejím zavolání je ignorován zbytek test case.
  • S = skipped. Toto označení je podobné předchozímu, s tím rozdílem, že jako skipped bychom měli označit testy, které nebylo možné v daném prostředí spustit. Například kvůli absenci nějakého rozšíření (gd, pdo, …). Označení se provádí zavoláním metody markTestSkipped($msg) opět s jedním nepovinným parametrem – důvodem přeskočení.

assertEquals vs. assertSame

Asertační metoda assertEquals není jediná, kterou PHPUnit nabízí. Je jich celá řada a detailně se jim bude věnovat příští díl tohoto seriálu. Dnes se podíváme ještě na jednu, a tou je assertSame. Očekává stejné parametry jako assertEquals, rozdíl mezi nimi je stejný jako mezi operátory == a ===. Tedy prosté porovnání vs. striktní porovnání, včetně typové shody. Ve většině případů si vystačíte s assertEquals (stejně jako s operátorem ==), assertSame s výhodou využijete třeba pro porovnání dvou polí, kde záleží na pořadí prvků.

$expected = array(0 => 1, 1 => 2, 2 => 3, 3 => 4);
$actual = array(1 => 2, 2 => 3, 0 => 1, 3 => 4);

$this->assertEquals($expected, $actual); // true
$this->assertSame($expected, $actual); // false

Pojmenování test case metod

Stejně jako assertEquals a assertSame, i ostatní asertační metody PHPUnit mají jako poslední, nepovinný, parametr textovou zprávu, která je zobrazena na výstupu při selhání testu. V praxi se ale spíše než tento parametr využívá postup, kdy se každý test case pojmenovává tak, aby bylo na první pohled jasné, co testuje. Například:

public function testDivisionByZeroThrowsException() {/* ... */}

Jednak je pak z výstupu jasné o jaký problém jde, a kromě toho PHPUnit u některých výstupních formátů umí camel-case názvy převádět na věty. Jde například o formát TestDox.

$ phpunit --testdox .
PHPUnit 3.6.11 by Sebastian Bergmann.

Calc
 [ ] Division by zero throws fatal error

Metody setUp a tearDown

Dnes se podíváme ještě na dvě užitečné metody PHPUnit – setUp a tearDown. První z nich jste si mohli všimnout u prvního vzorového testu. Jde o šablonovou metodu, která je frameworkem volána před každým jednotlivým testovacím případem. V praxi se používá pro přípravu prostředí před každým testem, např. instancializaci proměnných, které jsou testovacími případy používány.

PHPUnit nabízí i její opačnou variantu – metodu tearDown, která je volána vždy po dokončení každého testovacího případu. U této metody ale pozor na jedno nebezpečí – pokud dojde během testovacího případu k násilnému ukončení běhu skriptu (např. volání nedefinované funkce), pak tato metoda samozřejmě nebude zavolána! Její používání je tedy vždy třeba dobře zvážit. Určitě není dobrým nápadem na ni spoléhat a umisťovat do ní např. zavření file handlerů apod.

Příště…

To je z dnešních základů vše, jako cvičení si můžete napsat další testovací případy (např. ošetření a test chování metody divide při pokusu o dělení nulou apod.). Příště detailně rozebereme všechny asertační metody, které PHPUnit nabízí a také anotace, které kromě ulehčení práce poskytují i další možnosti nastavení chování frameworku PHPUnit.

Komentáře

Subscribe
Upozornit na
guest
24 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Filips

Pokud dojde k násilnému ukončení běhu skriptu tak se sice tearDown() nezavolá ale ukončí se i celý běh phpunitu. Php pak samo uvolní všechny alokované zdroje.
Naopak, tearDown() se velmi hodí pro uzavírání file handlerů, protože se provede vždy po každé testovací metodě i pokud její asserty selhaly. Je si totiž potřeba uvědomit, že pokud assert selže tak je vykonávání testovací metody ukončeno a tudíž se další příkazy v metodě neprovádějí.

jos

a v čem přesně tkví to nebezpečí? jak mi pomůže umístit ten kód jinam, když výsledek bude stejnej?

Patrik Votoček

test si po sobě třeba neuklidí v testovací DB

jos

z článku:

… pokud dojde během testovacího případu k násilnému ukončení běhu skriptu (např. volání nedefinované funkce), pak tato metoda samozřejmě nebude zavolána

po volání nedefinovaný funkce po sobě neuklidí tak jako tak

Filip Procházka

Pointou je, že tyhle věcí se mají čistit a připravovat v setUp(), tearDown() je zbytečnost.

jos

chápu že je to seriál, ale autor si asi tuhle pointu nenechal do dalších dílů a kdyby zrovna jo, tak by to snad rovnou řekl Filipsovi (tazatel)

čištění v setUp() mě taky napadlo, ale změnou produkčního kódu měnim i zdroje který sou ve hře, takže místo situace „občas to po sobě neuklidí => řešim špatnej zásah do produkčního kódu“ mam situaci „občas se to ne(před)uklidí dobře => řešim ukízecí mechanismus“

co si kdo zvolí je jeho věc, obojí může ovlivnit následnej běh testu, já bych* častěji bral to první, protože si myslim že ta situace nastává řidčeji, pro tebe je to zbytečnost, takže se budeš se setUp() prodírat jinejma problémama a autor seriálu se k řešení ještě nevyjádřil, každopádně pro něj je to rovnou nebezpečný, což je dost silný slovo

* „bych“, protože se si nemyslim že u nás tenhle „problém“ máme zapotřebí řešit, spousta věcí se vyřeší samotným zdechnutím php, do databáze nelezem a jediný co po sobě naše testy vobčas trousej sou náhodně pojmenovaný soubory a adresáře (dokonce i když testy proběhnou, pravděpodobně race condition destruktor versus antivir) a kolizí se nebojíme; ještě udělam reklamu – používáme testilenci, což je dost puristickej testovací nástroj, bohužel https://hg.sigpipe.cz/testilence/ je už dlouho down, takže v případě zájmu UTFG, nebo vydržet, znovuuvedení do provozu je slibovaný už dost dlouho

jk

Zajimalo by me, co nechapete na vete: tearDown nemusi byt zavovan, nespolehejte na to… Nerekl bych, ze si autor nechava dalsi vysvetleni na priste, spis nereaguje na pritrouble komentare… ;-)

jos

já tu větu chápu, ale strach z toho že se nezavolá nemam, je přitroublý že chci znát nebezpečí z toho plynoucí? pod článkem pro „začátečníky“?

EsoRimer

Pěkný článek, ale mě to nefunguje.
Musel jsem do zdrojáku s textem přidat requre souboru Calc.php.

A myslím že je zbytečné, aby phpunit procházel a hledal testy v adresáři src/, tak by ho bylo lepší volat místo „phpunit .“ jako „phpunit test“.

Každopádně se těším na další díly! :)

esorimer

Aha, to jsem asi přehlédl :)

lenoch

Me se ta myslenka automatizovanych testu libi, ale nikdo mi nedokazal uvest realny priklad, chyb jake skutecne nastavaji. Vzdycky je to nejaky priklad typu kalkulacka.
Dejme tomu ze vracim z databazove tabulky nejake objekty, podle podminky a ty pak treba aktualizuju. Pak se behem vyvoje prida nejaky parametr „skupina“ a ma se zohlednovat pri aktualizaci. A na jednom miste v aplikaci zapomenu tu podminku „skupina“ pridat, takze dojde k aktualizaci zaznamu, ktere by se aktualizovat nemely.
Jak mi v tom pomuzou unittesty?

Clary

Záleží na struktuře aplikace. Aktualizuješ přímo objekty nebo záznamy o nich v DB?
Jakým způsobem se má parametr skupina zohleďnovat při aktualizaci? Jak má vypadat podmínka s tímto parametrem? Jak bylo řečeno v druhém komentáři od autora článku, záleží hodně na pokrytí a na tom, aby byl kód napsán jako testovatelný.

lenoch

Ja to nechci nejak moc rozebirat, protoze se v problematice nevyznam. Zda se mi, ze testing je dobre mozny v pripade, kdy je aplikace napsana zpusobem output = funkcionalita(in­put) bez jakychkoli vedlejsich vlivu.

Ale v realne aplikaci mame treba databazi, sessions, javascript na klientovi, problemy prohlizece, nastaveni serveru a php, ruzne uzivatele, kteri mohou mit zcela jine konfigurace. To vsechno tvori prostredi, ktere ovlivnuje jestli program pobezi spravne nebo ne. Me pripada, ze to vsechno by v praxi muselo vstupovat do testu, pokud by mely byt dokonale.
A otestovat vsechny podoby tohoto prostredi se mi zda dost obtizne, kvuli mnozstvi moznosti. Proste vas nenapadne, ze chyba muze nastat pri kombinaci (x,y,u,v,w,z) a tak ji neotestujete.
A jednoduche testy typu assert(1+1 == 2) jsou zase k nicemu.

Ale snad se mylim, nemam v te oblasti zkusenosti – rad si prectu pokracovani.

EsoRimer

Tak třeba to nastavení serveru je pěkný příklad. Vyvíjím na svém počítači, všechny testy jsou ok. Pak to dám na server někam na demo a zkusím znova testy …

Jiný příklad: Píšu si metodu na serializaci a deserializaci objektů. Nejdřív napíšu test kde objekty serializuji, deserializuji a porovnávám výsledek s púvodním objektem. A programuji dokud test neprojde.

A když někdy v budoucnu upravím serializaci a zapomenu na úpravu deserializace, test to odhalí.

A ještě si můžu přidat do testu podmínku že serializovaný objekt nesmí být větší než xy bajtů, protože by mi to moc zatěžovalo síť. A když nějaký aktivista objekt nadměrně obohatí o atributy, tak to test zase odhalí.

atd, atd.

Clary

Samozřejmě testování návratové hodnoty funkce je to nejednodušší co může být, ale v praxi zase tolik takových jednoduchých věcí není. Databáze se obvykle simuluje nějakým testovacím adaptérem (např Zend_Test_DbAdapter ať navážu na zdejší seriál o Zendu), Sessions se zase bereou z nějakého jiného objektu (Environment, NetteHttpSes­sion), který se namockuje (zkráceně se vytvoří zástupný objekt mající námi požadované vlastnosti). Tyto objekty se předávají testované třídě potažmo metodě a poté můžeme testovat buď návratovou hodnotu testované metody nebo změny na těchto předaných objektech.
Samozřejmě lze testovat i s databází, ale to už nespadá do jednotkových testů. Stejně tak existují testovací prostředky na JavaScript ať už prostřednictvím opět jednotkových testů JavaScriptu (qUnit, Mocha) nebo testů celého prostředí např. pomocí Selenia, které aplikaci proklikává v prohlížeči jako reálný uživatel.

Tomáš

Vidíš to moc izolovaně. Testy jsou části vývoje a je záhodno brát je stejně vážně jako samotný kód. Testovací framework je nástroj, který tomu hodně pomáhá, ale teoreticky se bez něj obejdeš.

Ve tvém případě si prostě uděláš test, kde si nakrmíš databázi testovacími daty a kontrolouješ, že tvoje funkce vrací, co mají vracet (a zapisují, co mají zapisovat) podle zadání. Při nějaké změně nejdřív upravíš test (mimochodem, většinou je to míň kódu, než „ostrý“ kód). Potom pustíš testy a díváš se, kde všude ti to vylítlo. Ale to už jsem zabrousil do test driven development, me všichni zajdou takhle daleko.

jos

nakrmíš databázi testovacími daty

pod článkem o PHPUnit pro začátečníky není moc vhodný psát takový věci

Quark

Ale za zmínku to stálo. Myslímže když jde o testování, určitě je vhodné zmínit velení metodikou. A jestli to je TDD je zatím jedno. Aby totiž nedošlo zprvopočátku k mýlce, že testování je všehovšudy jen klikání do kalkulačky, zdali má ošetřeno dělení nulou :-)

jos
<?pseudohnus

iface db
{
  query($q);
}

class db_
implements
  db
{
  function query($q)
  {
    ...
  }
}

class mockDb_
implements
  db
{
  public $qs = array();
  function query($q)
  {
    $this->qs[] = $q;
  }
}

class updated
{
  function set($what, $val)
  {
    ...
  }
  function update(db $d)
  {
    $d->query($this->mkQuery());
  }
  ...
}

$u = new updated;
$db = new mockDb;
$u->set('whatever', 42);
$u->update($db);
assertEquals(array('update dual set whatever = 42'), $db->qs);
$db->qs = array();
$u->set('whatever', 42);
$u->update($db);
assertEquals(array(), $db->qs);

stačí to takhle?

Ondra K.

Seriál začíná slibně, těším se na další díly. Jen doufám, že brzy půjde více do „praktické hloubky“, jako např. právě zmiňované testy metod/funkcí pracujících s databází, se soubory a s jinými zdroji, které nelze obsáhnout přímo v PHP kódu.

EsoRimer

Třeba ještě se sítí :)

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.