Testování v PHP: XML konfigurace PHPUnit

V posledním díle první části seriálu se podíváme na možnosti XML konfigurace PHPUnit.
Seriál: Testování a tvorba testovatelného kódu v PHP (13 dílů)
- Testování a tvorba testovatelného kódu v PHP 13. 8. 2012
- Testování v PHP: Instalace a základy PHPUnit 27. 8. 2012
- Testování v PHP: asserty a constraints 10. 9. 2012
- Testování v PHP: praktický příklad 1. 10. 2012
- Testování v PHP: anotace 8. 10. 2012
- Testování v PHP: odstiňujeme závislosti 22. 10. 2012
- Testování v PHP: odstiňujeme závislosti II. 5. 11. 2012
- Testování v PHP: testy integrace s databází 19. 11. 2012
- Testování v PHP: testy integrace s databází II. 3. 12. 2012
- Testování v PHP: řízení běhu pomocí parametrů 7. 1. 2013
- Testování v PHP: XML konfigurace PHPUnit 21. 1. 2013
- Testování v PHP: tvorba testovatelného kódu 18. 2. 2013
- Testování v PHP: tvorba testovatelného kódu II. 11. 3. 2013
Nálepky:
V minulém díle seriálu jsme si ukázali možnosti řízení běhu PHPUnit pomocí parametrů příkazové řádky. Dnes dokončíme první část našeho seriálu a podíváme se na možnosti XML konfigurace frameworku.
Parametry
Stejně jako je možné řídit běh testů pomocí parametrů příkazové řádky, je možné analogické volby používat i v XML. Význam většiny atributů by nám měl být jasný, jde o „camelCase“ obdobu parametrů, které jsme si ukazovali minule.
<phpunit backupGlobals="true" backupStaticAttributes="false" bootstrap="/path/to/bootstrap.php" colors="false" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" forceCoversAnnotation="false" printerClass="PHPUnit_TextUI_ResultPrinter" printerFile="/path/to/ResultPrinter.php" processIsolation="false" stopOnError="false" stopOnFailure="false" stopOnIncomplete="false" stopOnSkipped="false" testSuiteLoaderClass="PHPUnit_Runner_StandardTestSuiteLoader" testSuiteLoaderFile="/path/to/StandardTestSuiteLoader.php" strict="false" verbose="false"> </phpunit>
Nová je tu jen čtveřice atributů:
- convertNoticesToExceptions, convertWarningsToExceptions, convertErrorsToExceptions – potlačení defaultního chování frameworku, kdy jsou odchytitelné chyby PHP převáděny na výjimky
- forceCoversAnnotation – omezení generování code coverage reportu pouze na ty části kódu, které jsou označeny anotací @covers
Sady testů
Možnost organizace testů do logických sad využijeme třeba v případě, kdy potřebujeme spouštět pouze určitou část našich testů. Jednou z možností, jak toho docílit, je používat anotaci @group – pak ale musíme důsledně dodržovat označování testů. Druhou a mnohem jednodušší možností jsou právě „test suites“ neboli sady testů. V konfiguračním souboru si vytvoříme jednotlivé sady a do nich zahrneme požadované soubory s testy nebo celé adresáře. Dodatečně můžeme sady testů omezit na určitou verzi PHP.
<testsuites> <testsuite name="FirstTestSuite"> <directory suffix="Test.php" phpVersion="5.3.0" phpVersionOperator=">=">/srv/project/tests/firstGroup</directory> <file phpVersion="5.3.0" phpVersionOperator=">=">/srv/project/tests/secondGroup/FooTest.php</file> <exclude>/srv/project/tests/firstGroup/bankAccount</exclude> </testsuite> <testsuite name="SecondTestSuite"> <directory>/srv/project/tests/secondGroup</directory> </testsuite> </testsuites>
V ukázce výše jsme si nadefinovali dvě sady testů: FirstTestSuite a SecondTestSuite. Do první sady jsme zahrnuli všechny testy z adresáře /srv/project/tests/firstGroup kromě adresáře /srv/project/tests/firstGroup/bankAccount. Budou spuštěny všechny testy ze souborů, jejichž název končí Test.php. Navíc jsme do sady přidali soubor /srv/project/tests/secondGroup/FooTest.php. Tato sada testů může být spuštěna pouze v prostředí s PHP ve verzi alespoň 5.3.0.
Druhá sada testů (SecondTestSuite) už je definována jednoduše – zahrnuli jsme do ní všechny testy z adresáře /srv/project/tests/secondGroup Budou prohledány všechny soubory bez ohledu na postfix, sada může být spuštěna v prostředí s jakoukoli verzí PHP, kompatibilní s aktuální verzí PHPUnit.
Už nám zbývá odhalit pouze jediné – jak spustit pouze požadovanou sadu. K tomuto slouží parametr příkazové řádky: –testsuite [pattern].
$ phpunit -c phpunit.xml --testsuite FirstTestSuite
Skupiny testů
Stejně jako pomocí přepínačů příkazové řádky, i v konfiguračním souboru můžeme definovat, které skupiny testů chceme do běhu zahrnout a které nikoli. V tomto případě už jsme odkázáni pouze na anotace @group nebo @author. Neplést prosím se sadami, které jsme si ukazovali výše. Zde se jedná pouze o obdobu přepínačů –group a –exclude-group.
Spuštění pouze těch testů, které jsou zařazeny (označeny anotací @group) do skupiny „unit“:
<groups> <include> <group>unit</group> </include> </groups>
Spuštění všech testů, kromě těch, které jsou zařazeny (označeny anotací @group) do skupiny „integration“:
<groups> <exclude> <group>integration</group> </exclude> </groups>
Filtrování souborů pro code coverage
Pomocí této direktivy můžeme ovlivnit, které zdrojové soubory budou přidány do generování reportu pokrytí kódu testy (code coverage) a které nikoli. Využití nalezneme třeba v momentě, kdy nechceme generovat code coverage pro soubory z knihoven třetích stran.
Definice se skládá z dvou částí – blacklist a whitelist. Význam obou by nám měl být zřejmý – v sekci blacklist uvedeme, které adresáře nebo konkrétní soubory si nepřejeme do code coverage zařadit, v sekci whitelist opak – které adresáře nebo konkrétní soubory chceme zařadit do code coverage. V každé ze sekcí je možné pro drobnější definici filtru použít ještě direktivu exclude, kterou říkáme „vše, kromě tohoto“.
Direktiva whitelist má jeden nepovinný parametr: processUncoveredFilesFromWhitelist (defaultní hodnota je true). Je-li nastavena na true, pak soubory, které nebyly zařazeny do code coverage, budou zahrnuty do výpočtu statistik (budou započítány počty jejich řádků).
<filter> <blacklist> <directory suffix=".php">/srv/project/lib/external</directory> <file>/srv/project/lib/loader.php</file> <exclude> <directory suffix=".php">/srv/project/lib/external/foo</directory> </exclude> </blacklist> <whitelist processUncoveredFilesFromWhitelist="true"> <directory suffix=".php">/srv/project</directory> <exclude> <directory suffix=".php">/srv/project/www</directory> <file>/srv/project/data/conf.php</file> </exclude> </whitelist> </filter>
Výše uvedený příklad zahrne do generování code coverage všechny soubory s příponou .php z adresářů /srv/project a /srv/project/lib/external/foo. Dále jsme manuálně zablokovali soubory /srv/project/lib/loader.php, /srv/project/data/conf.php a celý adresář /srv/project/www.
Logování výsledků
Možnosti nastavení logování výsledků testů jsou téměř shodné s možnostmi, které nabízí parametry příkazové řádky. Navíc jsou jen atributy pro generování HTML code coverage:
- charset: znaková sada výsledného HTML
- highlight: zvýraznění syntaxe jazyka
- lowUpperBound: horní procentuální hranice pokrytí kódu. Pokud je procentuální pokrytí kódu menší, pak je kód označen jako málo pokrytý testy.
- highLowerBound: spodní procentuální hranice pokrytí kódu. Pokud je procentuální pokrytí kódu větší, pak je kód označen jako hodně pokrytý testy.
<logging> <log type="coverage-html" target="/tmp/report" charset="UTF-8" highlight="false" lowUpperBound="35" highLowerBound="70"/> <log type="coverage-clover" target="/tmp/coverage.xml"/> <log type="coverage-php" target="/tmp/coverage.serialized"/> <log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/> <log type="json" target="/tmp/logfile.json"/> <log type="tap" target="/tmp/logfile.tap"/> <log type="junit" target="/tmp/logfile.xml" logIncompleteSkipped="false"/> <log type="testdox-html" target="/tmp/testdox.html"/> <log type="testdox-text" target="/tmp/testdox.txt"/> </logging>
Nastavení prostředí
V této konfigurační sekci můžeme ovlivňovat nastavení prostředí, ve kterém testy poběží (runtime změny direktiv v php.ini, include_path, …) nebo před-nastavit hodnoty superglobálních polí ($_GET, $_POST, …). Příklad uvedený v oficiální dokumentaci mluví za vše:
<php> <includePath>/some/dir</includePath> <ini name="foo" value="bar"/> <const name="foo" value="bar"/> <var name="foo" value="bar"/> <env name="foo" value="bar"/> <post name="foo" value="bar"/> <get name="foo" value="bar"/> <cookie name="foo" value="bar"/> <server name="foo" value="bar"/> <files name="foo" value="bar"/> <request name="foo" value="bar"/> </php>
Výše uvedené nastavení odpovídá (ve stejném pořadí) tomuto zápisu v PHP:
set_include_path('/some/dir;' . get_include_path()); ini_set('foo', 'bar'); define('foo', 'bar'); $GLOBALS['foo'] = 'bar'; $_ENV['foo'] = 'bar'; $_POST['foo'] = 'bar'; $_GET['foo'] = 'bar'; $_COOKIE['foo'] = 'bar'; $_SERVER['foo'] = 'bar'; $_FILES['foo'] = 'bar'; $_REQUEST['foo'] = 'bar';
Test listeners
Poslední kapitolou, na kterou se v souvislosti s PHPUnit podíváme, jsou tzv. Test listeners. Do češtiny by se toto označení si dalo přeložit jako „posluchač výsledků testů“, ale zůstaňme raději u původního názvu. Test listeners se používají k odchytávání výsledků („odposlouchávání“) z průběhu testování k jejim dalšímu zpracování.
Dalším zpracováním může být myšleno rozesílání mailem, logování (např. viz. https://github.com/benmatselby/phpunit-testlistener-mongo – logování do MongoDB), výpočet statistik a další. Ukážeme si vše na příkladu jednoduchého listeneru, který nám bude mailem hlásit selhání některého z testů.
Každý listener musí implementovat rozhraní PHPUnit_Framework_TestListener, které vyžaduje implementaci osmi metod:
- addError(PHPUnit_Framework_Test $test, Exception $e, $time)
Je volána při zachycení chyby v testovaném kódu nebo v testu samotném. Chyba je převedena na výjimku, která je předána jako druhý parametr. Třetím parametrem je čas, kdy došlo k zachycení chyby. - addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time)
Je volána při selhání testu. Selhání testu je převedeno na výjimku, která he předána jako druhý parametr. - addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time)
Je volána při nálezu testu, který je označen jako neúplný. - addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time)
Je volána při nálezu testu, který je označen k přeskočení. - startTestSuite(PHPUnit_Framework_TestSuite $suite)
Je volána při spuštění sady testů. - endTestSuite(PHPUnit_Framework_TestSuite $suite)
Je volána při ukončení sady testů. - startTest(PHPUnit_Framework_Test $test)
Je volána při spuštění každého test case. - endTest(PHPUnit_Framework_Test $test, $time)
Je volána při ukončení test case.
Náš vzorový test listener bude v průběhu testování sbírat chybové zprávy a po ukončení testování je odešle na zadanou e-mailovou adresu.
class EmailAddressListener implements PHPUnit_Framework_TestListener { private $mailto; private $message; public function __construct($mailto) { $this->mailto = $mailto; } public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) { $this->message .= "Error in" . $test->getName() . "n"; $this->message .= "Error message:" . $e->getMessage() . "n"; } public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) { $this->message .= "Failure in" . $test->getName() . "n"; $this->message .= "Error message:" . $e->getMessage() . "n"; } public function startTest(PHPUnit_Framework_Test $test) {} public function endTest(PHPUnit_Framework_Test $test, $time) {} public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) {} public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) {} public function startTestSuite(PHPUnit_Framework_TestSuite $suite) {} public function endTestSuite(PHPUnit_Framework_TestSuite $suite) { if ($this->message) { mail($this->mailto, "Test Failed at " . date("j.n.Y H:i:s"), $this->message); } } }
Poslední, co nám zbývá, je test listener připojit k testům. K tomuto účelu je možné použít direktivu listeners:
<listeners> <listener class="EmailAddressListener" file="EmailAddressListener.php"> <arguments> <string>tester@mydomain.com</string> </arguments> </listener> </listeners>
Příště
To už je z první části seriálu, kde jsme se seznámili s frameworkem PHPUnit, opravdu vše. Ve druhé části seriálu o testování se na celou problematiku podíváme z druhé strany – jak psát kód, který je testovatelný. Vše si ukážeme na příkladu refactoringu špatně navržené třídy do její lepší podoby.