Poznámka: Články budou popisovat základní použití uvedených technik bez ambicí o úplnost. Pokud máte zkušenosti s různými rozšířeními, jinými frameworky, jinými způsoby používání, zmiňte je, prosím, v diskuzi. Naším společným cílem by mělo být rozšíření poznání čtenářů Root.cz.
Terminologická poznámka: Místo termínu jednotkové testování se někdy používá termín testování komponent. Je to například v ISTQB studijních materiálech. A nutno říci, že termín testování komponent je významově přesnější než termín jednotkové testování. Na druhou stranu je ale termín jednotkové testování mnohem známější.
1. Užitečné odkazy
phpunit.de
– hlavní stránka PHPUnitphpunit.readthedocs.io
– dokumentace k PHPUnitcodeception.com
– hlavní stránka Codeceptionxunitpatterns.com
– ucelená kolekce antivzorů a dalších „nepravostí“www.robertdresler.cz/2012/02/jak-nepsat-jednotkove-testy.html
– další „antivzory“
2. Úvodní informace a použitá knihovna
Průběžné používání jednotkových testů by měla být pro dobrého programátora stejná samozřejmost jako průběžné psaní programátorské dokumentace. Psaní tohoto typu testů se považuje za vývojářskou (nikoliv testerskou) aktivitu a má se za to, že dobře napsaná sada těchto testů znamená poloviční úspěch celého vývoje.
Tento způsob testování je také nejlevnější, protože testy se píší a testování probíhá souběžně s vytvářením kódu, tzn. na chybu se přijde okamžitě a její oprava je velmi laciná. Vše totiž provádí jeden člověk – programátor. Další významnou sekundární výhodou je, že pokud programátor používá při vývoji souběžně jednotkové testy, nutí jej tento způsob práce, aby produkoval zdrojový kód, který je dobře testovatelný. To představuje vytváření malých (nikoliv rozsáhlých) metod, s jednoznačným účelem, s jasně definovaným rozhraním (kontraktem), co nejméně provázané s ostatním kódem atp. Toto všechno je v souladu s doporučeními ohledně zvyšování kvality kódu a na ně navázaných metrik.
Prostě jen velmi těžko najdete na používání jednotkových testů nějakou nevýhodu, kromě té, že z počátku jejich používání představují malou časovou investici navíc. Ta se ale brzy vrátí.
Základem pro psaní jednotkových (unit) testů je knihovna xUnit. Tvůrce konceptu jednotkového testování Kent Beck ji připravil nejprve pro Smalltalk. Tento koncept se ukázal natolik kvalitní / výhodný / využitelný, že existuje ve variantách pro různé programovací jazyky. Pro PHP existuje PHPUnit, přičemž druhý rozšířený framework je Codeception.
Cílem tohoto článku nebude jejich vzájemné porovnání. Na základě porovnání již provedeného (září 2021) byl pro další práci vybrán PHPUnit.
PHPUnit je velmi rozšířený způsob psaní a spouštění testů, v podstatě se dá považovat za de facto průmyslový standard. V současné době je součástí většiny RAD (PhpStorm, Eclipse, …), které poskytují testům všemožnou podporu. Ta spočívá např. v automatickém generování kostry testů, grafické nadstavbě pro jejich spouštění a vizualizaci výsledků, možnosti hromadného spouštění testů apod.
Podíváme-li se do nejbližší historie, vidíme, že tvůrce PHPUnit pan Sebastian Bergmann má ve zvyku vydávat hlavní verze vždy začátkem února:
- 7.0 – 2.2.2018
- 8.0 – 1.2.2019
- 9.0 – 7.2.2020
Poznámka: V dalších příkladech bude používána verze 9.5.4, která má logo:
PHPUnit je distribuován běžnými způsoby – Composer, atp. V dále uvedených příkladech bude použito „ruční“ nastavení za významné podpory PhpStorm, kdy se stahuje soubor phpunit.phar
(4,4 MB).
Varování: Zde popisované možnosti PHPUnit jsou jen subjektivním výběrem těch nejužitečnějších. Ve skutečnosti jsou možnosti PHPUnit větší. Pokud zde není popsáno řešení problému, kterému čelíte, hledejte v dokumentaci PHPUnit.
3. Ukázka jednoduché entitní třídy pro testování
Abychom mohli začít testovat, potřebujeme k tomu testovaný kód. V této části bude ukázán příklad takovéhoto jednoduchého testovaného kódu.
Třída HodnoceniPredmetu
slouží pro záznam známky z předmětu. Je napsaná velmi zjednodušeně, ale obsahuje všechny potřebné části pro ukázky základů jednotkového testování.
Budeme-li vytvářet její instance, lze vytvořit instanci pouze s uvedeným názvem předmětu, tj. bez jeho hodnocení – pak je známka nastavena na hodnotu DOSUD_NEHODNOCENO
. V tomto případě vrací metoda isNehodnoceno()
hodnotu true
.
Metoda setZnamka(int znamka)
nejprve ověřuje, zda je nastavovaná známka v povoleném rozsahu. Pokud ne, je vyhozena run-time výjimka \InvalidArgumentException
.
<?php namespace app\zaklad; class HodnoceniPredmetu { // konstanty pro známkování const VYBORNE = 1; const NEDOSTATECNE = 5; const DOSUD_NEHODNOCENO = 0; const DOSUD_NEHODNOCENO_SLOVY = "N/A"; private $nazev; private $znamka; public function __construct(string $nazev, int $znamka=self::DOSUD_NEHODNOCENO) { $this->nazev = $nazev; $this->setZnamka($znamka); } public function getNazev() : string { return $this->nazev; } public function getZnamka() : int { return $this->znamka; } /** * 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); } } /** * Zjistí, zda je předmět dosud nehodnocen * @return true, pokud je předmět nehodnocen */ public function isNehodnoceno() : bool { return ($this->znamka == self::DOSUD_NEHODNOCENO); } public function __toString() : string { if ($this->isNehodnoceno() == true) { return $this->nazev . ": " . self::DOSUD_NEHODNOCENO_SLOVY . "<br>"; } else { return $this->nazev . ": " . $this->znamka . "<br>"; } } }
3.1. „Klasické“ ověření funkčnosti
Termín „klasické“ je zde použit ve významu „jak by postupoval programátor třídy HodnoceniPredmetu
při neznalosti jednotkového testování“. Tato část zde bude uvedena pro budoucí srovnání s principem jednotkového testování.
Máme-li testovanou třídu (termín pro ni by byl SUT – System Under Test), můžeme napsat pokusný index.php
, který ji bude ověřovat:
<?php require_once("autoload.php"); use app\zaklad\HodnoceniPredmetu; $hodnoceni = new HodnoceniPredmetu("Fyzika", 1); echo $hodnoceni->__toString(); $predmet = new HodnoceniPredmetu("Matika"); echo $predmet->__toString(); echo "nehodnoceno:" . $predmet->isNehodnoceno() . "<br>"; $predmet->setZnamka(3); echo $predmet->__toString();
Přičemž struktura projektu je:
Obsah souboru autoload.php
. BTW: Zde není autoload nezbytný. Je uveden proto, že tentýž soubor bude používán i ve všech dalších příkladech:
<?php set_include_path(get_include_path() . PATH_SEPARATOR . __DIR__ . '\\src'); spl_autoload_extensions('.class.php,.trait.php,.inc.php,.php'); spl_autoload_register();
Po spuštění index.php
dostaneme:
To, zda je výsledek očekávaný či nikoliv, musíme ověřit pohledem a případně i nějakou další analýzou kódu. Zde se jedná např. o výsledek nehodnoceno:1
, který ve skutečnosti znamená nehodnoceno:true
Při použití tohoto postupu lze opravdu najít všechny zásadní problémy a hned je opravit. ALE! Do budoucna je zcela ztraceno veškeré úsilí, které programátor do tohoto ověřování vložil. Pod pojmem „úsilí“ se myslí jak psaní konkrétních výkonných částí kódu, tak i mentální vyhodnocování, zda ověřování prokázalo správnou či nesprávnou funkci programu.
Místo tohoto postupu můžeme začít psát regulérní testovací případy, které poslouží lépe a navíc budou moci být trvalým doplňkem této třídy. Nad nejasnými případy nám bude stačit přemýšlet pouze jednou při prvním spuštění, vyhodnocení všech ostatních spuštění již provede počítač, respektive použitá knihovna testů.
4. Použití testů s využitím PHPUnit v PhpStorm
Poznámka: Příklad je ukazován na PhpStorm ve verzi 2021.1.
Poznámka: PHPUnit lze samozřejmě používat i bez podpory jakéhokoliv RAD. Na stránkách PHPUnit Take the first steps / Getting Started si můžete přečíst, jak na to.
4.1. Příprava adresáře test
Je dobrým zvykem oddělit kód testů od výkonného kódu. To umožňuje v projektu snadněji udržovat pořádek a při nasazení do produkce nenasazovat i testy.
Pomocí Mark Directory as z kontextového menu se označí jako testovací.
Výsledek, kdy je adresářtest
podbarven zeleně, což je standardní barva pro testy:
4.2. Vygenerování kostry testovací třídy
Příprava kostry třídy testovacích případů a následné spouštění testů je v PhpStorm maximálně zautomatizováno.
V editoru otevřeme třídu HodnoceniPredmetu
a pomocí kontextového menu Code / Generate … zvolíme Test
Zde postupně:
- Vybereme šablonu PHPUnit
- Doplníme podadresář, kam bude soubor testu uložen, tj.
\app\zaklad
Z hlediska udržení pořádku je vhodné mít testy ve stejné adresářové struktuře, jako je testovaný kód - Zaškrtneme
setZnamka
, tj. metodu, pro kterou chceme vygenerovat test - Všechna ostatní nastavení PhpStorm inteligentně vyplní, včetně jména nové testovací třídy. Testovací případy jsou typicky uloženy ve třídě, která se jmenuje
TestovanáTřídaTest
, zdeHodnoceniPredmetuTest
Výsledek bude:
4.3. Doplnění zdrojového kódu testu
Do předpřipravené kostry metody testSetZnamka()
doplníme tentýž kód (bez příkazů echo
), kterým jsme zkoušeli funkčnost třídy v index.php
, tj.:
$predmet = new HodnoceniPredmetu("Matika"); $predmet->setZnamka(3);
PhpStorm nám nabídne doplnění:
use app\zaklad\HodnoceniPredmetu;
Ovšem pokud jsou testy ve stejném jmenném prostoru, je toto doplnění zbytečné.
Jako poslední příkaz zavoláme metodu pro automatické vyhodnocení testu, která je ze zděděného TestCase
:
$this->assertEquals(3, $predmet->getZnamka());
Výsledek bude:
Nyní je připraven testovací případ a je nutné provést konfiguraci testů.
4.4. Natažení a instalace PHPUnit
Spustíme testy ze třídy HodnoceniPredmetuTest
(zelenou šipkou vlevo od zdrojového kódu), přičemž v pravém horním rohu je výpis, že čeká na konfiguraci.
Pokus o spuštění dosud nenakonfigurované knihovny PHPUnit dle očekávání selže:
Ale je již předpřipravena konfigurace pro spuštění testu:
Pomocí File / Settings vybereme typ testovacího frameworku PHPUnit Local:
Zvolíme, že budeme používat lokální verziphpunit.phar
a následně se objeví hlášení, že není instalován, ovšem společně s odkazem na automatický download.
Po kliknutí na download musíme určit, do jakého adresáře budephpunit.phar
uložen. Adresář je předpřipravený, stačí jen odkliknout.
Pokud s PHPUnit začínáme (což je náš případ) je výhodné phpunit.phar
uložit do přednastaveného adresáře projektu, protože PhpStorm nám bude moci z něj našeptávat nápovědu. Nevýhodou ale je, že pro víc projektů bychom museli mít na disku víc kopií phpunit.phar
.
Takže až budeme PHPUnit rutinně ovládat a našeptávání nebudeme potřebovat, lze phpunit.phar
uložit na jedno místo, odkud bude sdílen všemi projekty.
Pomocí automatického downloadu se stahuje nejnovější verze PHPUnit, takže při svých pokusech pravděpodobně uvidíte vyšší verzi.
Poslední nutnou akcí je zadání, kde je uložen autoloader, zde označovaný jako bootstrap:
Zvolíme náš autoload.php
:
Výsledná konfigurace frameworku je: (Nezapomeneme závěrečně odkliknout OK.)
4.5. Skutečné spuštění testů
Když stejným způsobem spustíme testy, dostaneme:
5. Části testovacího případu
Vygenerovaná a námi doplněná testovací třída HodnoceniPredmetuTest
obsahuje některé dosud neznámé příkazy:
<?php namespace app\zaklad; use app\zaklad\HodnoceniPredmetu; use PHPUnit\Framework\TestCase; class HodnoceniPredmetuTest extends TestCase { public function testSetZnamka() { $predmet = new HodnoceniPredmetu("Matika"); $predmet->setZnamka(3); $this->assertEquals(3, $predmet->getZnamka()); // self::assertEquals(3, $predmet->getZnamka()); } }
Poznámka:
Zdrojové kódy xUnit testů často obsahují „magická čísla“ (zde 3
), což není považováno za nedokonalost kódu.
Význam jednotlivých částí kódu:
use PHPUnit\Framework\TestCase;
- – umožní používat třídu
TestCase
– viz dále extends TestCase
- – naše třída testů dědí od třídy
TestCase
, což umožní používat všechny její metody a konstrukce, např.$this->assertEquals()
public function testSetZnamka()
- – hlavička metody, která představuje jeden testovací případ; může být typu
void
, tj.:
public function testSetZnamka() : void
Název metody je odvozen od názvu testované metodysetZnamka
a musí začínat slovemtest
. Podle tohoto identifikátoru PHPUnit pozná, že se jedná o testovací metodu, kterou má spustit.
Jméno metody často končí pojmenováním toho, co se testuje (zde nepoužito), případně pořadovým číslem testovacího případu (viz dále). Je běžné, že jedna metoda je testována více testovacími případy – pozitivní a negativní testy. V tomto uvedeném příkladu se používá jeden pozitivní test.
V těle metody by měla být alespoň jedna – typicky právě jedna – metodaassert...
Toto pravidlo se občas rozumně porušuje, dále uvidíme více assert metod v jednom testovacím případě. $predmet = new HodnoceniPredmetu("Matika");
- – vytvoření objektu testované třídy – běžný Php kód
$predmet->setZnamka(3);
- – nastavení známky – běžný Php kód
$this->assertEquals(3, $predmet->getZnamka());
- – vlastní test s využitím metody
assertEquals()
ze třídyTestCase
z PHPUnit.
Zde platí, že první skutečný parametr je očekávaná hodnota a druhý skutečný parametr je aktuální hodnota objektupredmet
. Pokud se budou hodnoty obou parametrů rovnat, tento testovací případ proběhne úspěšně.
Poznámka:
Testy píšeme tak, aby prošly, tj. tak, jak očekáváme, že bude program správně fungovat. Toto pravidlo platí pro pozitivní i negativní testy.
5.1. Pravidlo Arange – Act – Assert
Test by měl být napsán tak, aby obsahoval tři části:
- Arange – nastavení testovaného objektu do stavu, v němž bude testován; často je to vytvoření tohoto objektu
$predmet = new HodnoceniPredmetu("Matika");
- Act – provedení testované aktivity, tj. nejčastěji volání prověřované metody
$predmet->setZnamka(3);
- Assert – porovnání očekávaného stavu testovaného objektu se stavem skutečným
$this->assertEquals(3, $predmet->getZnamka());
Tyto tři části by měly být co nejjednodušší, ideálně vždy jen jedna řádka kódu.
6. PHPUnit test odhalil chybu
Jedná se o neúspěšný průběh testu, kdy test selhal.
Platí zásada, že pokud v celé testovací třídě alespoň jeden testovací případ selhal (zde v ukázce je zatím pouze jeden selhaný TC), je celý test chybně.
Selhaný testovací případ je ohlášen chybovou zprávou a tato zpráva je pak vypsána způsobem závislým na RAD.
Například pro metodu, ve které je změněna očekávaná hodnota na 2
public function testSetZnamka() { $predmet = new HodnoceniPredmetu("Matika"); $predmet->setZnamka(3); $this->assertEquals(2, $predmet->getZnamka()); }
Po spuštění dostaneme červený výsledek testu a vidíme důvod selhání testu:
Je možné kliknout i na podrobnější zobrazení rozdílu, ale v tomto případě nedostaneme oproti textovému výpisu žádnou informaci navíc.
Význam má zobrazení rozdílu např. už u porovnání řetězců (ukázka je z jiného testu).
Důležitější je informace v samotném editoru, kde zčervenají ikony u testů, které selhaly:
6.1. Rozdíl mezi Error a Failure
Častý problém, který je poměrně záludný, je v případě, že testy se jeví jako by selhávaly (tj. objevily chybu v testované třídě), ovšem jedná se o falešné hlášení (false positive).
Celý problém si ukážeme na testovací třídě FailureErrorTest
. Ta má dvě testovací metody, přičemž:
- metoda
testIsNehodnoceno_Failure()
očekávaně selže - v metodě
testIsNehodnoceno_Error()
assert vůbec neproběhne, protože je v ní zapomenuté vytvoření objektu třídyHodnoceniPredmetu
<?php namespace app\zaklad; use app\zaklad\HodnoceniPredmetu; use PHPUnit\Framework\TestCase; class FailureErrorTest extends TestCase { public function testIsNehodnoceno_Failure() { $predmet = new HodnoceniPredmetu("Matika"); $this->assertFalse($predmet->isNehodnoceno()); } public function testIsNehodnoceno_Error() { $predmet = null; // chybně předpokládáme, že instance HodnoceniPredmetu je již vytvořena $this->assertFalse($predmet->isNehodnoceno()); } }
Při pozornějším pohledu na výsledky testů v PhpStorm je třeba rozlišovat výsledky typu Failure a Error:
1. Failure – test selže, protože selže metoda assertXY()
2. Error – test selže, protože je v něm nějaká programová chyba; typický případ je, kdy v těle testu voláme nějakou instanční metodu, ale v metoděsetUp()
(viz dále) – nebo někde jinde – nebyla instance vytvořena