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.