Asserty
V minulém díle našeho seriálu o testování v PHP jsme si ukázali jak PHPUnit nainstalovat a také jsme si zkusili napsat první, jednoduchý, test. V testu jsme použili assert assertEquals a krátce jsme se seznámili s jeho striktnější obdobou: assertSame.
V dnešním díle si podrobně představíme všechny asserty, které PHPUnit nabízí, včetně již uvedených assertEquals a assertSame. Zdrojové kódy s jednoduchými příklady všech assertů, na kterých si vše můžete vyzkoušet, najdete na mém Githubu:
https://github.com/josefzamrzla/serial-testovani-v-php
Assert vs. constraint
Ještě než se vrhnete do zkoumání jednotlivých assertů, je dobré si ujasnit rozdíl mezi pojmy assert a constraint. Je to prosté – assert zjišťuje, zda zkoumaná hodnota vyhovuje uvedenému omezení (constraint). Neboli v pseudokódu:
assertThat(constraint->matches(value) )
Toto je základem všech assertů (nebo asertačních metod, chcete-li) v PHPUnit a umožňuje nám napsat si i vlastní constraints, což si ukážeme v závěru dnešního dílu.
tl;dr
Následuje referenční popis assertů, platný k verzi 3.6.11. Víceméně stejný popis naleznete i v originální dokumentaci, ale ta ne vždy odráží realitu a mnohdy vás může docela zmást. Proto doporučuji níže uvedený přehled alespoň rychle prolétnout, budu upozorňovat na všechny „špeky“, které jsou mi do dnešního dne známy.
Přehled assertů v PHPUnit
Testování shody
assertEquals
assertEquals($expected, $actual[, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE])
Základní assert, který je ostatními constraints interně hojně využíván. Ověřuje prostou shodu předpokladu ($expected) se skutečnou hodnotou ($actual). Prostou shodou je myšlena shoda bez typové kontroly, tedy stejně jako v případě operátoru: ==.
Třetím, nepovinným, parametrem může být textová zpráva, která se zobrazí v případě selhání testu. Tento parametr se vyskytuje u všech ostatních assertů, proto jej už nebudu popisovat. Další nepovinné parametry:
- $delta – pokud jsou obě testované hodnoty (předpoklad i skutečná hodnota) numerické, pak je možné tímto parametrem definovat maximální přípustný rozdíl.
- $maxDepth – historický parametr, v PHPUnit už není používán.
- $canonicalize – pokud jsou oběma vstupy (předpoklad i skutečná hodnota) pole a tento parametr je nastaven na bool(true), pak jsou pole nejprve seřazena a teprve potom porovnána.
- $ignoreCase – pokud je parametr bool(true), jsou oba vstupy porovnávány jako lower-case. Platí i v případě pole řetězců.
assertSame
assertSame($expected, $actual, $message = '')
Striktně ověřuje předpoklad se skutečnou hodnotou. Ověřuje i typovou shodu, v případě asociativních polí záleží i na pořadí prvků.
assertNull
assertNull($actual, $message = '')
Striktně ověřuje, zda skutečná hodnota je NULL.
assertTrue
assertTrue($condition, $message = '')
Striktně ověřuje, zda je výsledek podmínky bool(true).
assertFalse
assertFalse($condition, $message = '')
Striktně ověřuje, zda je výsledek podmínky bool(false).
assertEmpty
assertEmpty($actual, $message = '')
Ověřuje, zda je skutečná hodnota prázdná. K ověření používá funkci empty a jako „prázdné“ označuje: bool(false), null, prázdný řetězec, prázdné pole apod.
Testování větší/menší než
assertGreaterThan
assertGreaterThan($expected, $actual, $message = '')
Ověřuje, zda skutečná hodnota je větší než předpoklad. Je možné použít i pro porovnání řetězců.
assertGreaterThanOrEqual
assertGreaterThanOrEqual($expected, $actual, $message = '')
Ověřuje, zda skutečná hodnota je větší než nebo rovna předpokladu. Je možné použít i pro porovnání řetězců.
assertLessThan, assertLessThanOrEqual
assertLessThan($expected, $actual, $message = '')
assertLessThanOrEqual($expected, $actual, $message = '')
Opačné varianty předchozích dvou assertů, tedy: menší a menší nebo rovno.
Testování obsahu
assertArrayHasKey
assertArrayHasKey($key, array $array, $message = '')
Ověřuje, zda pole obsahuje očekávaný klíč. Assert pracuje pouze s typem: array.
assertContains
assertContains($needle, $haystack, $message = '', $ignoreCase = FALSE, $checkForObjectIdentity = TRUE)
Ověřuje, zda kolekce $haystack obsahuje předpokládaný prvek. Assert pracuje s typy: array, string a Traversable (iterovatelné objekty). Dalšími parametry jsou:
- $ignoreCase – při porovnání je ignorována velikost písmen. Má význam pouze u typu: string.
- $checkForObjectIdentity – použít striktní porovnání. Má význam pouze u typů: Traversable.
assertContainsOnly
assertContainsOnly($type, $haystack, $isNativeType = NULL, $message = '')
Ověřuje, zda kolekce $haystack obsahuje pouze prvky očekávaného typu. Assert pracuje pouze s typy: array a Traversable. Třetím parametrem $isNativeType určujeme, zda očekáváme nativní PHP typ nebo vlastní typ.
assertCount
assertCount($expectedCount, $haystack, $message = '')
Ověřuje, zda kolekce $haystack obsahuje očekávaný počet prvků. Assert pracuje s typy: array, Countable a Iterator.
assertRegExp
assertRegExp($pattern, $string, $message = '')
Ověřuje, zda řetězec $string vyhovuje očekávanému regulárnímu výrazu. Používá PCRE výrazy (funkci preg_match).
assertStringStartsWith, assertStringEndsWith
assertStringStartsWith($prefix, $string, $message = '')
assertStringEndsWith($suffix, $string, $message = '')
Ověřují, zda řetězec začíná nebo končí očekávaným řetězcem.
assertStringEqualsFile
assertStringEqualsFile($expectedFile, $actualString, $message = '', $canonicalize = FALSE, $ignoreCase = FALSE)
Ověřuje, zda obsah řetězce $actualString odpovídá očekávanému řetězci, uloženému v souboru $expectedFile. Assert je vhodný např. pro ověřování (porovnávání) dlouhých řetězců. Volitelné parametry odpovídají stejnojmenným parametrům assertEquals.
assertStringMatchesFormat
assertStringMatchesFormat($format, $string, $message = '')
Ověřuje, zda obsah řetězce $string odpovídá očekávanému formátu. Pro zápis formátu je možné použít následující značky:
- %e: oddělovač adresářů, např. „/“ v Unix-based operačních systémech.
- %s: Alespoň jeden jakýkoli znak, kromě znaku konce řádku.
- %S: Žádný nebo jakýkoli znak, kromě znaku konce řádku.
- %a: Alespoň jeden jakýkoli znak, včetně znaku konce řádku.
- %A: Žádný nebo jakýkoli znak, včetně znaku konce řádku.
- %w: Jakýkoli počet netisknutelných znaků.
- %i: Jakékoli číslo typu integer se znaménkem, např. +3142, –3142.
- %d: Jakékoli číslo typu integer bez znaménka, např. 123456.
- %x: Jakýkoli hexadecimální znak, tzn. 0–9, a-f, A-F.
- %f: Jakékoli desetinné číslo, např. 3.142, –3.142, 3.142E-10, 3.142e+10.
- %c: Jakýkoli jeden znak.
assertStringMatchesFormatFile
assertStringMatchesFormatFile($formatFile, $string, $message = '')
Pracuje stejně jako předchozí assert, jen s tím rozdílem, že očekávaný formát je uložen v souboru $formatFile.
assertFileEquals
assertFileEquals($expected, $actual, $message = '', $canonicalize = FALSE, $ignoreCase = FALSE)
Ověřuje, zda jsou obsahy dvou souborů shodné. Volitelné parametry odpovídají stejnojmenným parametrům assertEquals.
assertFileExists
assertFileExists($filename, $message = '')
Ověřuje, zda soubor existuje.
assertInstanceOf
assertInstanceOf($expected, $actual, $message = '')
Ověřuje, zda proměnná $actual je instancí očekávaného typu.
assertInternalType
assertInternalType($expected, $actual, $message = '')
Ověřuje, zda proměnná $actual je očekávaného typu.
Reflexe
assertClassHasAttribute
assertClassHasAttribute($attributeName, $className, $message = '')
Pomocí reflexe ověřuje, zda třída obsahuje očekávaný atribut bez ohledu na to, zda je statický nebo instanční.
assertClassHasStaticAttribute
assertClassHasStaticAttribute($attributeName, $className, $message = '')
Pomocí reflexe ověřuje, zda třída obsahuje očekávaný statický atribut.
assertObjectHasAttribute
assertObjectHasAttribute($attributeName, $object, $message = '')
Pomocí reflexe ověřuje, zda instance obsahuje očekávaný atribut bez ohledu na to, zda je statický nebo instanční.
Kromě těchto tří assertů, pracujících s reflexí, nabízí PHPUnit obdobu téměř všech assertů, uvedených v tomto díle, v podobě assertAttribute…. Tzn. např. assertAttributeEquals, assertAttributeSame, assertAttributeContains a mnoho dalších. Jak už název napovídá, tyto asserty se používají pro testování obsahu atributů tříd nebo instancí. Jejich funkčnost je shodná s jejich předlohami, proto je nebudu explicitně popisovat.
HTML/XML
assertEqualXMLStructure
assertEqualXMLStructure(DOMElement $expectedElement, DOMElement $actualElement, $checkAttributes = FALSE, $message = '')
Ověřuje, zda je XML element $actualElement shodný s očekávaným $expectedElement. Pokud je parametr$checkAttributes nastaven na bool(true), pak je ověřována shoda včetně atributů (existence, ne hodnot!).
assertXmlFileEqualsXmlFile
assertXmlFileEqualsXmlFile($expectedFile, $actualFile, $message = '')
Ověřuje, zda je obsah XML souboru $actualFile shodný s očekávaným souborem $expectedFile.
assertXmlStringEqualsXmlFile
assertXmlStringEqualsXmlFile($expectedFile, $actualXml, $message = '')
Ověřuje, zda obsahem řetězce $actualXml je XML struktura shodná s obsahem souboru $expectedFile. Vhodný pro porovnávání větších XML struktur – očekávanou XML strukturu můžeme uložit mimo kód testu.
assertXmlStringEqualsXmlString
assertXmlStringEqualsXmlString($expectedXml, $actualXml, $message = '')
Ověřuje, zda obsahem řetězce $actualXml je XML struktura shodná s předpokládanou XML strukturou v proměnné $expectedXml.
assertTag
assertTag($matcher, $actual, $message = '', $isHtml = TRUE)
Patrně největší chameleon mezi asserty v PHPUnit. Ověřuje, zda obsah řetězce $actual vyhovuje kritériím, nastaveným v „matcheru“ $matcher. Tzv. matcher (opět se obávám, že neexistuje vhodný překlad) je asociativní pole obsahující různé sady nebo kombinace podmínek. Při prvním pohledu vás asi jejich množství vyděsí, ale opravdu to není tak hrozné. Oficiální dokumentace obsahuje příliš obecný popis a chybová zpráva „Failed asserting that false is true“ vám asi taky moc nepomůže.
Vše je o tom, že musí existovat element, o kterém platí všechny podmínky v matcheru najednou. Např. existuje element s názvem (tag) div a zároveň s atributem id=„uniqId“.
$matcher = array("tag" => "div", "id" => "uniqId");
Dalšími podmínkami potom můžeme upřesňovat výběr, včetně zanořování matcherů do sebe. Tzn. můžeme hledat element, který vyhovuje podmínkám matcheru a zároveň jeho potomek vyhovuje podmínkám jiného matcheru a zároveň jeho sourozenec vyhovuje podmínkám … atd. Protože se assertTag používá i pro testování XML, budu v dalším textu používat spíše označení element než tag. Další možnosti matcheru:
- tag: element má požadovaný název. Ač se to na první pohled nezdá, toto je poměrně podivné omezení. Pokud jej totiž nepoužijete, tak to neznamená, že podmínce vyhovuje libovolně pojmenovaný element, ale pouze element ze skupiny: <a>, <abbr>, <acronym>, <address>, <area>, <b>, <base>, <bdo>, <big>, <blockquote>, <body>, <br>, <button>, <caption>, <cite>, <code>, <col>, <colgroup>, <dd>, <del>, <div>, <dfn>, <dl>, <dt>, <em>, <fieldset>, <form>, <frame>, <frameset>, <h1>, <h2>, <h3>, <h4>, <h5>, <h6>, <head>, <hr>, <html>, <i>, <iframe>, <img>, <input>, <ins>, <kbd>, <label>, <legend>, <li>, <link>, <map>, <meta>, <noframes>, <noscript>, <object>, <ol>, <optgroup>, <option>, <p>, <param>, <pre>, <q>, <samp>, <script>, <select>, <small>, <span>, <strong>, <style>, <sub>, <sup>, <table>, <tbody>, <td>, <textarea>, <tfoot>, <th>, <thead>, <title>, <tr>, <tt>, <ul>, <var>. To se v dokumentaci nedočtete :-)
- id: element má požadovaný atribut ID.
- attributes: element má požadované atributy, které odpovídají asociativnímu poli.
- content: element má požadovaný textový obsah.
- parent: rodičovský element odpovídá kritériím dalšího matcheru (vnořování matcherů).
- child: alespoň jeden z přímých potomků elementu odpovídá kritériím dalšího matcheru.
- ancestor: alespoň jeden libovolný předek odpovídá kritériím dalšího matcheru.
- descendant: alespoň jeden libovolný potomek odpovídá kritériím dalšího matcheru.
- children: element má seznam potomků, který odpovídá následujícím podmínkám:
- count: počet potomků vyhovujících matcheru only je roven zadanému (není možné nastavit omezení na nulu!).
- less_than: počet potomků vyhovujících matcheru only je menší než zadaný.
- greater_than: počet potomků vyhovujících matcheru only je větší než zadaný.
- only: matcher omezující potomky, kteří budou započítáni. I tady pozor – rodičovský element musí mít POUZE potomky, které tomuto matcheru odpovídají!
Dodatečný parametr $isHtml rozlišuje, zda se názvy elementů (omezení tag) budou hledat case-sensitive módu ($isHtml = false) nebo case-insensitive ($isHtml = true).
S tímto assertem si můžete užít hodně zábavy, ať už v pozitivním nebo negativním smyslu. Tak či tak, dobře se hodí pro integrační nebo akceptační testy, kdy pracujeme s finálním html výstupem aplikace.
assertSelectCount
assertSelectCount($selector, $count, $actual, $message = '', $isHtml = TRUE)
Ověřuje existenci elementu(ů) v řetězci $actual pomocí CSS selektorů. V oficiální dokumentaci je parametr $selector označen za array, ale nenechte se tím zmást, selektor se zadává jako string. V selektoru je možné používat jen základní CSS selekci:
- libovolný potomek, např.: div div p
- přímý potomek, např.: div > p
- id, class, např.: div#main .content (! pouze v html módu !)
- atributy s konkrétní hodnotou, např.: foo[bar=„123“]
- atributy obsahující slovo, např.: foo[bar~=„word“]
- atributy obsahující substring, např.: foo[bar*=„some substring“]. U všech atributů platí, že víceslovné hodnoty musí být uvozeny uvozovkami, ne apostrofy!
Druhým parametrem $count potom nastavujeme očekávaný počet. Může nabývat celkem tří typů:
- boolean – očekáváme pouze exitenci (true) nebo absenci (false) elementu
- integer – očekáváme přesný počet elementů
- array – očekáváme počet elementů odpovídající zadanému rozsahu. Klíčemi pole jsou operátory: >, >=, <, <= , hodnotami pak krajní meze rozsahu. Např.: array(„>“ ⇒ 1, „<=“ ⇒ 10) očekává alespoň jeden, ale maximálně deset elementů.
Třetí parametr $isHtml určuje, zda chceme pracovat v HTML nebo XML módu. Zde je rozdíl např. v tom, jak hledáme elementy s parametrem ID. V HTML módu můžeme jednoduše použít selektor #, zatímco v XML módu musíme použít selektor hodnota atributu [id=„…“].
assertSelectEquals, assertSelectRegExp
assertSelectEquals($selector, $content, $count, $actual, $message = '', $isHtml = TRUE)
assertSelectRegExp($selector, $pattern, $count, $actual, $message = '', $isHtml = TRUE)
Pro tyto dva asserty platí to samé, jako pro assertSelectCount, jen můžete navíc nastavit omezení na obsah hledaných elementů.
assertSelectEquals – omezení obsahu na prostou shodu s očekávanou hodnotou $content
assertSelectRegExp – omezení obsahu na shodu vyhovující PCRE výrazu $pattern
Vlastní constraints
Jeden assert nám v seznamu ještě chybí – assertThat.
assertThat($value, PHPUnit_Framework_Constraint $constraint, $message = '')
Jak už jsem psal na začátku tohoto dílu – jde o assert, který je základem všech výše uvedených. Např. assertNull si můžeme představit jako:
public function assertNull($actual, $message = '') { $this->assertThat($actual, new PHPUnit_Framework_Constraint_IsNull()); }
Jde samozřejmě pouze o pseudokód, ale víceméně takto je assertNull skutečně implementován. Stejně tak i ostatní asserty. A modří už vědí – úplně stejně můžeme volat vlastní constraints.
ContainsSubstringConstraint
Předpokládejme, že chceme testovat obsah pole řetězců, ale assertContains nám nevyhovuje, protože obsahem našeho pole jsou řetězce, které jsou poměrně dlouhé a my nechceme testovat shodu na celý řetězec, stačí nám shoda se sub-řetězcem. Hledáme tedy něco jako assertContainsSubstring. Bohužel žádný takový constraint neexistuje, takže nám nezbude nic jiného, než si jej napsat. Není to nic težkého – vlastní constraint vytvoříme jako potomka třídyPHPUnit_Framework_Constraint, pojmenujeme jej třeba ContainsSubstringConstraint.
class ContainsSubstringConstraint extends PHPUnit_Framework_Constraint { public function toString() { return "string array contains "; } }
Protože rodičovská třída PHPUnit_Framework_Constraint implementuje rozhraníPHPUnit_Framework_SelfDescribing, musíme implementovat metodu toString, která náš constraint popisuje.
Budeme potřebovat porovnávat dva subjekty, proto musíme přidat konstruktor vyžadující jeden parametr – hledaný řetězec. Aby měl constraint smysl a nevracel nám falešně pozitivní shody, vložíme rovnou i omezení na parametr – musí se jednat o neprázdný řetězec nebo číslo. Nic jiného neumožníme.
class ContainsSubstringConstraint extends PHPUnit_Framework_Constraint { private $needle; public function __construct($needle) { if (!((is_string($needle) || is_numeric($needle)) && strlen($needle))) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } $this->needle = (string)$needle; } public function toString() { return "string array contains "; } }
Nyní už stačí jen implementovat šablonovou metodu matches($other), která je rodičovskou třídou volána, a která by měla vracet bool(true) pokud parametr $other vyhovuje podmínkám, v ostatních případech bool(false). Parametrem $other je v našem případě pole nebo iterovatelná kolekce, ve které budeme řetězec hledat.
class ContainsSubstringConstraint extends PHPUnit_Framework_Constraint { private $needle; public function __construct($needle) { if (!((is_string($needle) || is_numeric($needle)) && strlen($needle))) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } $this->needle = (string)$needle; } public function toString() { return "string array contains "; } protected function matches($other) { if (!(((is_array($other) && count($other))) || ($other instanceof Traversable))) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'array'); } foreach ($other as $item) { if (is_string($item) && strstr($item, $this->needle) !== false) { return true; } } return false; } }
Stejně jako v případě konstruktoru, i zde omezíme parametr pouze na očekávaný typ: array. Protože se nám tento constraint může v budoucnu hodit i pro iterovatelné kolekce, povolíme i instance implementující rozhraní Traversable. Zbývá už jen danou kolekci nebo pole prohledat a při prvním nálezu požadovaného řetězce vrátit bool(true).
Tím je vlastní constraint hotov. Příklad použití:
$strings = array( "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse congue tincidunt mi...", "Maecenas euismod lorem at leo pretium vehicula. Aliquam libero nunc, blandit at accumsan ...", "Sed risus urna, varius ac sodales sed, sodales at erat. Sed eu tellus ac mi interdum ..." ); // should be found $this->assertThat($strings, new ContainsSubstringConstraint("Aliquam libero nunc"));
Logická spojování constraints
Kromě takto prostého použití můžeme používat i jiné constraints, které slouží k logickému spojování jiných constraints:
- PHPUnit_Framework_Constraint_And – součin, logické AND.
Uvnitř constraint lze volat jako: $this->logicalAnd() - PHPUnit_Framework_Constraint_Or – součet, logické OR.
Uvnitř constraint lze volat jako: $this->logicalOr() - PHPUnit_Framework_Constraint_Not – negace, logické NOT.
Uvnitř constraint lze volat jako: $this->logicalNot() - PHPUnit_Framework_Constraint_Xor – exkluzivní součet, logické XOR.
Uvnitř constraint lze volat jako: $this->logicalXor()
Díky těmto constraints můžeme poskládat širokou škálu různých omezení, jak ze stávajících constraints, tak z našich vlastních. Příklad 1.: zadaný řetězec nesmí být nalezen
$strings = array( "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse congue tincidunt mi...", "Maecenas euismod lorem at leo pretium vehicula. Aliquam libero nunc, blandit at accumsan ...", "Sed risus urna, varius ac sodales sed, sodales at erat. Sed eu tellus ac mi interdum ..." ); // tento retezec nesmi byt soucastni pole retezcum, jinak fail $this->assertThat($strings, $this->logicalNot(new ContainsSubstringConstraint("This string should not be found.")));
Příklad 2. zadaný řetězec musí být nalezen a zároveň kolekce musí mít pouze prvky typu string
$strings = array( "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse congue tincidunt mi...", "Maecenas euismod lorem at leo pretium vehicula. Aliquam libero nunc, blandit at accumsan ...", "Sed risus urna, varius ac sodales sed, sodales at erat. Sed eu tellus ac mi interdum ..." ); // $strings obsahuje pouze retezce a zaroven obsahuje retezec $this->assertThat($strings, $this->logicalAnd( new PHPUnit_Framework_Constraint_TraversableContainsOnly("string"), new ContainsSubstringConstraint("Aliquam libero nunc") ) );
To je pro dnešek vše, minule slíbené anotace si necháme na příště, i tak se nám tento díl rozlezl víc, než jsem čekal. Stáhněte se zdrojové kódy a vše vyzkoušejte. Příště se kromě už zmíněných anotací podíváme na CLI parametry a bootstrap xml, tedy na možnosti nastavení běhu PHPUnit. Opět s řadou tipů, upozornění a vychytávek.
Přehled komentářů