3. díl - Testování v PHP - Dokončení unit testů a best practices

PHP Testování Testování v PHP - Dokončení unit testů a best practices

Unicorn College ONEbit hosting Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V minulé lekci o testování webových aplikací v PHP jsme si nainstalovali framework Codeception a vygenerovali svůj první PHPUnit test. Dnes pokryjeme testy naši jednoduchou třídu, uvedeme si dostupné asserční metody a unit testy v PHP dovršíme přehledem best practices.

Pokrytí třídy testy

Metody setUp() a tearDown() se zavolají před, resp. po každém testu v této třídě. To je pro nás velmi důležité, jelikož podle best practices chceme, aby byly testy nezávislé. Obvykle tedy před každým testem připravujeme znovu to samé prostředí, aby se vzájemně vůbec neovlivňovaly. O dobrých praktikách se zmíníme detailněji později. Do třídy si přidejme atribut $kalkulacka a v metodě setUp() v něm vždy vytvořme čerstvě novou kalkulačku pro každý test. Pokud by ji bylo ještě třeba dále nastavovat nebo bylo třeba vytvořit další závislosti, byly by také v této metodě. Metodu testMe() odstraníme:

class KalkulackaTest extends \PHPUnit_Framework_TestCase
{
    private $kalkulacka;

    protected function setUp()
    {
        $this->kalkulacka = new Kalkulacka();
    }

    protected function tearDown()
    {
    }

}

Máme vše připraveno k přidávání samotných testů. Jednotlivé metody budou vždy začínat na "test" a budou testovat jednu konkrétní metodu z třídy Kalkulacka, typicky pro několik různých vstupů. Pokud vás napadá proč metody označujeme prefixy, umožňuje nám to vytvořit si i pomocné metody, které můžeme v daném testu využívat a které nebudou pokládány za testy. PhpStorm nám totiž testy (metody začínající na "test") automaticky spustí a vypíše jejich výsledky.

Přidejme následujících 5 metod:

public function testScitani()
{
        $this->assertEquals(2, $this->kalkulacka->secti(1, 1));
        $this->assertEquals(1.42, $this->kalkulacka->secti(3.14, -1.72), '', 0.001);
        $this->assertEquals(2/3, $this->kalkulacka->secti(1/3, 1/3), '', 0.001);
}

public function testOdcitani()
{
        $this->assertEquals(0, $this->kalkulacka->odecti(1, 1));
        $this->assertEquals(4.86, $this->kalkulacka->odecti(3.14, -1.72), '', 0.001);
        $this->assertEquals(2/3, $this->kalkulacka->odecti(1/3, -1/3), '', 0.001);
}

public function testNasobeni()
{
        $this->assertEquals(2, $this->kalkulacka->vynasob(1, 2));
        $this->assertEquals(-5.4008, $this->kalkulacka->vynasob(3.14, -1.72), '', 0.001);
        $this->assertEquals(0.111, $this->kalkulacka->vynasob(1/3, 1/3), '', 0.001);
}

public function testDeleni()
{
        $this->assertEquals(2, $this->kalkulacka->vydel(4, 2));
        $this->assertEquals(-1.826, $this->kalkulacka->vydel(3.14, -1.72), '', 0.001);
        $this->assertEquals(1, $this->kalkulacka->vydel(1/3, 1/3));
}

/**
 * @expectedException InvalidArgumentException
 */
public function testDeleniVyjimka()
{
        $this->kalkulacka->vydel(2, 0);
}

K porovnávání výstupu metody s očekávanou hodnotou používáme poděděné metody assert*. Dají se volat i staticky, ale my zůstaneme u instančního použití. Nejčastěji asi použijete assertEquals(), která přijímá jako první parametr očekávanou hodnotu a jako druhý parametr hodnotu aktuální. Toto pořadí je dobré dodržovat, jinak budete mít hodnoty ve výsledcích testů opačně. Jak asi víte, desetinná čísla jsou v paměti počítače reprezentována binárně (jak jinak :) ) a to způsobí určitou ztrátu jejich přesnosti a také určité obtíže při jejich porovnávání. Proto musíme v tomto případě zadat i čtvrtý parametr a to je delta, tedy kladná tolerance, o kolik se může očekávaná a aktuální hodnota lišit, aby test stále prošel. Třetím parametrem je chybová hláška, pokud test neproběhne. Většinou ji není důvod zadávat, pokud je test dobře pojmenovaný a z hodnot jednoduše poznáme, který assert se nepovedl.

Všimněte si, že zkoušíme různé vstupy. Sčítání netestujeme jen jako 1 + 1 = 2, ale zkusíme celočíselné, desetinné i negativní vstupy, odděleně, a ověříme výsledky. V některých případech by nás mohla zajímat také maximální hodnota datových typů a podobně.

Poslední test ověřuje, zda metoda vydel() opravdu vyvolá výjimku při nulovém děliteli. Jak vidíte, nemusíme se zatěžovat s try-catch bloky, stačí nad metodu přidat PHP anotaci @expectedException a uvést zde třídu výjimky, která se očekává. Pokud výjimka nenastane, test selže. Pro testování více případů vyvolání výjimky tímto způsobem by bylo třeba přidat více metod.

Dostupné assert metody

Kromě metody assertEquals() můžeme použít ještě mnoho dalších, určitě se snažte použít tu nejvíce vyhovující metodu, zpřehledňuje to hlášky při selhání testů a samozřejmě i následnou opravu. Seznam assert metod je poměrně vyčerpávající a můžete si jej jednoduše prohlédnout v IDE, zmiňme si tedy jen ty nejdůležitější:

  • assertContain­s($jehla, $kupka) - Zkontroluje, zda $kupka (pole) obsahuje danou hodnotu ($jehla).
  • assertCount($o­cekavanyPocet, $kolekce) - Zkontroluje, zda má $kolekce $ocekavanyPocet prvků.
  • assertFalse($hod­nota) - Zkontroluje, zda je hodnota false.
  • assertTrue($hod­nota) - Zkontroluje, zda je hodnota true.
  • assertNotEqual­s($ocekavanaHod­nota, $hodnota) - Zkontroluje, zda hodnoty NEjsou stejné. Podobná "Not" metoda je pro většinu assertů, další zde již nebudeme zbytečně zmiňovat.
  • assertGreater­Than($ocekava­naHodnota, $hodnota) - Zkontroluje, zda je $hodnota větší než $ocekavanaHodnota.
  • assertGreater­ThanOrEqual($o­cekavanaHodno­ta, $hodnota) - Zkontroluje, zda je $hodnota větší nebo rovna $ocekavanaHodnota.
  • assertLessThan($o­cekavanaHodno­ta, $hodnota) - Zkontroluje, zda je $hodnota menší než $ocekavanaHodnota.
  • assertLessTha­nOrEqual($oce­kavanaHodnota, $hodnota) - Zkontroluje, zda je $hodnota menší nebo rovna $ocekavanaHodnota.
  • assertNull($hod­nota) - Zkontroluje, zda je hodnota null.
  • assertSame($o­cekavanaHodno­ta, $hodnota) - Funguje stejně jako assertEquals(), ale kontroluje i shodu datových typů.

Jsou zde i připravené asserty pro testování atributů, řetězců (např. zda něčím začíná), polí, složek, souborů a XML.

assertThat()

Velmi zajímavá je ještě metoda assertThat(), která umožňuje alternativní přístup k assercím. Např. v Javě (PHPUnit poměrně jasně vychází z JUnit) tento způsob přináší navíc další možnosti kontroly datových typů, v PHP si jej zmiňme spíše jen pro zajímavost a ukažme si, jak by vypadal první assert z našich testů pomocí assertThat(). Připomeňme si původní variantu:

$this->assertEquals(2, $this->kalkulacka->secti(1, 1));

A verze s assertThat():

$this->assertThat(
        $this->kalkulacka->secti(1, 1),
        $this->equalTo(
                2
        )
);

Výhodou je, že zápis vypadá jako anglická věta. Nevýhodou je vyšší objem kódu a rekurzivní zanořování. Pro složitější asserty může být tento způsob výhodný.

Spuštění testů

Testy spustíme příkazem:

test run unit

Uvidíme výsledky, které vypadají nějak takto:

> C:\xampp\php\php.exe codecept.phar run unit
Codeception PHP Testing Framework v2.2.10
Powered by PHPUnit 5.7.17 by Sebastian Bergmann and contributors.

Unit Tests (5) -----------------------------------------------------------------
+ KalkulackaTest: Scitani (0.00s)
+ KalkulackaTest: Odcitani (0.00s)
+ KalkulackaTest: Nasobeni (0.00s)
+ KalkulackaTest: Deleni (0.01s)
+ KalkulackaTest: Deleni vyjimka (0.00s)
--------------------------------------------------------------------------------


Time: 254 ms, Memory: 11.00MB

OK (5 tests, 13 assertions)

Process finished with exit code 0 at 02:11:58.
Execution time: 328 ms.

Pokud vám Codeception hlásí problém s nedostupností příkazu "php", otevřete konfigurační soubor codeception.yml a do sekce settings přidejte hodnotu lint: false.

...
settings:
    bootstrap: _bootstrap.php
    colors: false
    memory_limit: 1024M
    lint: false
...

V určitých verzích se jinak může špatně vyhodnotit výstupní stav.

Zkusme si nyní udělat v kalkulačce chybu, např. zakomentujme vyvolávání výjimky při dělení nulou a vraťme vždy hodnotu 1:

public function vydel($a, $b)
{
        //if ($b == 0)
        //      throw new \InvalidArgumentException("Nelze dělit nulou!");
        return 1;
}

A spusťme znovu naše testy:

> C:\xampp\php\php.exe codecept.phar run unit
Codeception PHP Testing Framework v2.2.10
Powered by PHPUnit 5.7.17 by Sebastian Bergmann and contributors.

Unit Tests (5) -----------------------------------------------------------------
+ KalkulackaTest: Scitani (0.00s)
+ KalkulackaTest: Odcitani (0.00s)
+ KalkulackaTest: Nasobeni (0.00s)
x KalkulackaTest: Deleni (0.01s)
x KalkulackaTest: Deleni vyjimka (0.00s)
--------------------------------------------------------------------------------


Time: 252 ms, Memory: 11.00MB

There were 2 failures:

---------
1) KalkulackaTest: Deleni
 Test  tests\unit\KalkulackaTest.php:testDeleni
Failed asserting that 1 matches expected 2.
#1  C:\Users\David\PhpstormProjects\kalkulacka\tests\unit\KalkulackaTest.php:39
#2  KalkulackaTest->testDeleni
#3  C:\Users\David\PhpstormProjects\kalkulacka\codecept.phar:7

---------
2) KalkulackaTest: Deleni vyjimka
 Test  tests\unit\KalkulackaTest.php:testDeleniVyjimka
Failed asserting that exception of type "InvalidArgumentException" is thrown.
#1  C:\Users\David\PhpstormProjects\kalkulacka\codecept.phar:7

FAILURES!
Tests: 5, Assertions: 11, Failures: 2.

Process finished with exit code 1 at 02:51:07.
Execution time: 312 ms.

Vidíme, že chyba je zachycena a jsme na ni upozorněni. Neprošel jak test dělení, tak test vyvolání výjimky. Můžeme kód vrátit zpět do původního stavu.

Best practices

Již v minulé lekci jsme nakousli best practices. Jelikož to je k unit testům v PHP vše, pojďme si na závěr vyjmenovat jakých častých chyb se vyvarovat, abychom dosáhli kvalitního výsledku.

  • Testujeme specifikaci, nikoli kód. Testy nikdy nepíšeme podle kódu nějaké metody, ale zamýšlíme se nad tím, k čemu metoda reálně slouží a co vše ji může přijít jako vstup.
  • Testujeme obecné knihovny, ne konkrétní logiku aplikace. Pokud je logika důležitá a obecná, měla by být vyčleněná do samostatné knihovny a ta by měla být poté testována.
  • Každý test by měl být úplně nezávislý na ostatních testech. Scénář by měl proběhnout i když metody libovolně proházíme a žádná metoda by po sobě neměla zanechávat nějaké změny (v souborech, v databázi a podobně), které by ovlivnily další metody. K dosažení tohoto chování často připravujeme prostředí pro jednotlivé metody v before metodách a případně po nich ještě provedeme úklid v after metodách. To samé platí i pro celé testy.
  • Každý test by měl dopadnout vždy stejně, bez ohledu na to, kdy jej spustíme. Pozor na testování generátorů náhodných výstupů a na práci s datem a časem.
  • Neprovádějte duplicitní porovnání, pokud nějaký vstup již ověřuje jiný test, neprovádějte toto ověření znovu (DRY).
  • Každý scénář testuje jen jednu jednotku (třídu). Váš software by měl být navržený tak, aby byl rozdělený na menší třídy, které mají minimální závislosti na ostatních a proto se dají jednoduše a nezávisle testovat (vzory high cohesion a low coupling).
  • Pokud testy vyžadují externí služby, měli bychom je tzv. mockovat. Tím vytváříme "falešné" služby se stejným rozhraním, které obvykle jen podstrkují testovací data. Využitím skutečných služeb bychom porušili nezávislost testů, jelikož by se navzájem začaly ovlivňovat. Méně elegantní řešení je vždy na začátku nastavit a na konci vrátit původní stav služeb.
  • Jako platí i všude jinde, vyhněte se zavádějícím názvům testů (jako vypocet(), vyjimka() a podobně). Programátoři často pojmenovávají testy i větším počtem slov, aby se poznalo co dělají. Běžně bychom to u metod neměli dělat, jelikož každá metoda dělá jen jednu činnost, ale u testů dává někdy smysl pojmenovat metody např. i takto obskurně vypoctiKvadratickouRovniciAZadejZaporneKoeficienty(), protože test často testuje více vstupů. V pojmenovávání testů byste měli být konzistentní. Nebojte se ani komentářů.
  • Testy by měly proběhnout rychle, jelikož v praxi obvykle testujeme všechny části aplikace různými typy testů a všechny časy se dokáží nasčítat do nepříjemné pauzy.

Vaše první unit testy nemusí být perfektní, stačí krátce otestovat to nejdůležitější. Uvidíte, že vám časem začnou selhávat a odhalovat chyby v implementaci. Čím je aplikace větší, tím o větší pokrytí testy (test code coverage) bychom se měli snažit. V příštích lekcích se podíváme na testy akceptační a necháme si automaticky proklikat naši aplikaci v prohlížeči :)


 

Stáhnout

Staženo 2x (2.89 MB)
Aplikace je včetně zdrojových kódů v jazyce PHP

 

 

Článek pro vás napsal David Čápka
Avatar
Jak se ti líbí článek?
4 hlasů
Autor pracuje jako softwarový architekt a pedagog na projektu ITnetwork.cz (a jeho zahraničních verzích). Velmi si váží svobody podnikání v naší zemi a věří, že když se člověk neštítí práce, tak dokáže úplně cokoli.
Unicorn College Autor se informační technologie naučil na Unicorn College - prestižní soukromé vysoké škole IT a ekonomie.
Miniatura
Všechny články v sekci
Testování v PHP
Aktivity (3)

 

 

Komentáře

Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zatím nikdo nevložil komentář - buď první!