11. Parametrizované testy
Představují důležitou možností PHPUnit, kdy je možné připravit sadu parametrů, nad kterými bude opakovaně spouštěn jeden anotovaný testovací případ. Výhodných případů užití nalezneme postupně celou řadu, např. pro testy mezních hodnot apod.
Od běžných dříve popisovaných testů se liší tím, že se u nich musí vždy specifikovat alespoň jeden datový zdroj. Jednotlivé typy parametrizovaných testů budou v následujícím popisu rozděleny podle typu a charakteru tohoto datového zdroje.
11.1. Malý počet parametrů jednoho typu – @testWith
Má-li test relativně málo parametrů (cca max. 10, přičemž toto omezení je dáno pouze subjektivní čitelností zdrojového kódu testu), je výhodné použít anotaci @testWith
.
Tato anotace je (v dokumentačním komentáři) doplněna seznamem parametrů v hranatých závorkách, které jsou předávány do takto anotované testovací metody. Platí pravidlo, že každý parametr musí být na samostatné řádce.
Toto je trochu omezující/nepraktické (z důvodu narůstajícího počtu řádek komentáře) v případě, že parametr je pouze jedna hodnota, tj. není v N-ticích.
Vazba tohoto seznamu parametrů na kód testu se pak provede tím, že testovací metoda má formální parametry, jejichž počet a typ je odvozen od parametrů anotace @testWith
.
Příklad použití bude ukázán na metodě setZnamka()
třídy HodnoceniPredmetu
/** * Nastavuje známku v rozsahu od VYBORNE do NEDOSTATECNE * @param znamka nastavovaná známka * @throws InvalidArgumentException pokud je známka mimo rozsah */ public function setZnamka(int $znamka) : void { if ($znamka >= self::VYBORNE && $znamka <= self::NEDOSTATECNE) { $this->znamka = $znamka; } elseif ($this->znamka === null && $znamka == self::DOSUD_NEHODNOCENO) { // prvni nastaveni v konstruktoru $this->znamka = $znamka; } else { throw new \InvalidArgumentException($znamka); } }
V příkladě je ukázka otestování platného a neplatného rozsahu známky.
Pozor: Hodnota [1]
u testNeplatnyRozsahZnamky()
je záměrná chyba, aby byl vidět způsob selhání testu.
<?php namespace app\parametrizovane; use app\skutecnytest\HodnoceniPredmetu; use PHPUnit\Framework\TestCase; class TestWithTest extends TestCase { /** * @testWith [1] * [2] * [3] * [4] * [5] */ public function testPlatnyRozsahZnamky(int $znamka) : void { $predmet = new HodnoceniPredmetu("Matika", $znamka); $this->assertEquals($znamka, $predmet->getZnamka()); } /** * @testWith [-1] * [0] * [1] * [6] */ public function testNeplatnyRozsahZnamky(int $znamka) : void { $predmet = new HodnoceniPredmetu("Matika"); $this->expectException(\InvalidArgumentException::class); $predmet->setZnamka($znamka); } }
Po spuštění se pro testPlatnyRozsahZnamky()
zobrazí:
Pro testNeplatnyRozsahZnamky()
s úmyslnou chybou se zobrazí:
11.2. Malý počet parametrů N-tice – @testWith
Pomocí anotace @testWith
je možné definovat libovolnou N-tici parametrů, dále nazývanou element. Jeho omezení je pouze v tom, že každý element musí být pole vyhovující definici v JSON.
Typickým elementem je dvojice [vstupní_hodnota, očekávaný_výstup]
. Ovšem snadno si dokážeme představit trojice [vstupní_hodnota, očekávaný_výstup, přesnost]
či jiné smysluplné N-tice.
Použití N-tic bude ukázáno na testech triviální testované třídy NasobilkaPeti
.
<?php namespace app\parametrizovane; class NasobilkaPeti { public static function vypocti(int $cislo) : int { return $cislo * 5; } }
V příkladu je ukázka testu malé násobilky opět se záměrnou chybou [4, 21]
<?php namespace app\parametrizovane; use app\parametrizovane\NasobilkaPeti; use PHPUnit\Framework\TestCase; class TestWithNticeTest extends TestCase { /** * @testWith [1, 5] * [2, 10] * [3, 15] * [4, 21] * [5, 25] */ public function testMalaNasobilka(int $vstup, int $ocekavano) : void { $this->assertEquals($ocekavano, NasobilkaPeti::vypocti($vstup)); } }
Po spuštění se zobrazí:
11.3. Větší počet N-tic parametrů – @dataProvider
Pomocí anotace @dataProvider
se lze odkázat na jméno instanční metody vracející jako array seznam (vypočtených/připravených/získaných) parametrů.
Jiným návratovým typem této metody může být místo seznamu hodnot objekt implementující rozhraní Iterator
. Součástí dokumentace k PHPUnit je kompletní ukázka třídy implementující toto rozhraní, která umožňuje načíst data z CSV souboru.
Metoda pro přípravu dat nesmí mít žádné formální parametry.
Užitečná věc je, že vracené pole může být klíčované. Hodnota klíče se pak vypisuje jako název datasetu, což zajistí přehlednější (libovolně přehledný) výpis výsledků.
Ukázka „ruční“ přípravy klíčovaného ( '1 * 5'
) elementu jako pole dvou prvků typu int
. Opět je zanesena záměrná chyba '4 * 5' => [4, 21]
.
Poznámka: Samozřejmě, že o smysluplnosti takovéto „ruční“ přípravy lze diskutovat. Nicméně z mého pohledu je zápis přehlednější, než tomu bylo u seznamu N-tic v dokumentačním komentáři u anotace @testWith
. Další možnost využití této „ruční“ přípravy bych viděl pro rychlé prototypování testu, kdy se po ověření nahradí tělo metody skutečným kódem – viz následující část.
<?php namespace app\parametrizovane; use app\parametrizovane\NasobilkaPeti; use PHPUnit\Framework\TestCase; class DataProviderKlicTest extends TestCase { /** * @dataProvider pripravaDat */ public function testMalaNasobilka(int $vstup, int $ocekavano) : void { $this->assertEquals($ocekavano, NasobilkaPeti::vypocti($vstup)); } public function pripravaDat() : array { return [ '1 * 5' => [1, 5], '2 * 5' => [2, 10], '3 * 5' => [3, 15], '4 * 5' => [4, 21], '5 * 5' => [5, 25] ]; } }
Po spuštění se zobrazí:
11.4. N-tice parametrů vytvořené programově – @dataProvider
Výše uvedené použití anotace @dataProvider
, kdy jsou data „natvrdo“ zadána v kódu, má určitý význam, ale praktické využití @dataProvider
je samozřejmě v tom, že metoda bude připravovat data programově.
A tato programová příprava má dva typické způsoby. V tom prvním jsou testovací data vypočítána, často i s nějakým prvkem pseudonáhodnosti. A ve druhém způsobu jsou načítána ze souboru – viz další část.
Ukázka programové přípravy elementu jako pole dvou prvků typu int
, které bude opět klíčované.
<?php namespace app\parametrizovane; use app\parametrizovane\NasobilkaPeti; use PHPUnit\Framework\TestCase; class DataProviderTest extends TestCase { /** * @dataProvider pripravaDat */ public function testMalaNasobilka(int $vstup, int $ocekavano) : void { $this->assertEquals($ocekavano, NasobilkaPeti::vypocti($vstup)); } public function pripravaDat() : array { $pole = array(); for ($i = 1; $i <= 5; $i++) { $element = array($i, $i * 5); $klic = $i . " * 5"; $pole[$klic] = $element; } return $pole; } }
Po spuštění se zobrazí:
11.5. N-tice parametrů načtená ze souboru – @dataProvider
Modifikací předchozího příkladu by bylo možné připravit seznam libovolných N-tic, kdy N-tice je typu array. Může se ale stát, že nám typ array z nějakého důvodu nevyhovuje, Případně se v naší testovací sadě vícekrát vyskytuje nějaká konkrétní struktura dat. Pak je možné uvažovat o použití návrhového vzoru Přepravka. Tento návrhový vzor má oproti předchozímu array tu výhodu, že jako objekt může poskytovat další služby, které do něj můžeme libovolně doprogramovat. V našem případě to bude metoda metoda __toString()
, která bude využita pro správnou informaci o datasetu při výpisu parametrizovaného testu.
Poznámka: Příklad může vypadat zbytečně složitě, ale je to ukázka, jak vytvořit a předat libovolnou N-tici jako objekt. A objekt Přepravky samozřejmě nemusí vzniknout jen čtením ze souboru.
Bude načítán soubor seznam-predmetu.csv
s následujícím obsahem:
Matika;1 Fyzika;2 Programování v Php;3
Třída přepravky PrepravkaPredmet
doplněná o metodu __toString()
.
Poznámka: Kód Přepravky je značně zjednodušen – jsou použity public
atributy místo getrů.
<?php namespace app\parametrizovane; class PrepravkaPredmet { public $nazev; public $znamka; public function __construct(string $nazev, int $znamka) { $this->nazev = $nazev; $this->znamka = $znamka; } public function __toString() : string { return "[nazev='" . $this->nazev . "', znamka=" . $this->znamka . "]"; } }
Třída vlastního testu DataProviderSouborTest
. Jak již bylo zmíněno, klíčem, tj. tím, co se vypisuje jako identifikace datasetu, je řetězec z __toString()
.
<?php namespace app\parametrizovane; use app\parametrizovane\PrepravkaPredmet; use app\skutecnytest\HodnoceniPredmetu; use PHPUnit\Framework\TestCase; class DataProviderSouborTest extends TestCase { /** * @dataProvider pripravaDat */ public function testNastaveniPredmetu(PrepravkaPredmet $pp) : void { $predmet = new HodnoceniPredmetu($pp->nazev, $pp->znamka); $this->assertEquals($pp->nazev, $predmet->getNazev()); $this->assertEquals($pp->znamka, $predmet->getZnamka()); } public function pripravaDat() : array { $pole = array(); $obsah = file_get_contents("..\..\..\seznam-predmetu.csv"); $obsah = trim($obsah); $radky = explode("\n", $obsah); foreach ($radky as $radka) { $casti = explode(";", trim($radka)); $predmet = new PrepravkaPredmet($casti[0], $casti[1]); $klic = $predmet->__toString(); $pole[$klic] = array($predmet); } return $pole; } }
Po spuštění se zobrazí:
Závěr
Předchozí minisérie o logování v PHP a tato o základech jednotkového testování pomocí PHPUnit byly připravovány s cílem poskytnout informace potřebné/důležité/zajímavé pro možné zvyšování kvality PHP aplikací.
Původní záměr byl dále pokračovat lehce pokročilejšími tématy z oblasti testování, jako je například mockování, problematika pokrytí kódu apod.
Protože však čtenost článků nebyla příliš vysoká, zdá se mi, že tato problematika z nějakých důvodů, které mi nepatří zde rozebírat, čtenáře příliš neoslovila. Z tohoto důvodu v historicky krátké době žádné pokračování neplánuji.
Pokud vás přesto články zaujaly, můj styl – například používání češtiny ;-)
– vám vyhovoval či minimálně nevadil a měli byste zájem o něco víc, můžete mne zkusit kontaktovat a domluvit se na nějaké formě další spolupráce.