Testování v PHP: asserty a constraints

V dnešním díle si podrobně představíme asserty, které PHPUnit nabízí, a zkusíme si napsat vlastní constraint.
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:
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.
tohle je v podstatě kopie referenční příručky :-(
@see tl;dr Upozorňoval jsem předem, že (alespoň co se assertů týče) jde o referenční popis.
…. ale, tohle jsem vidět nechtěl, to mám v podrobném manuálu
ak si chce niekto precitat/vytvorit pomerne zlozitu aplikaciu tdd stylom, uplne krok za krokom a nevadi mu ze je to pre ruby on rails tak odporucam „Rails 3 in Action“ (zdrojaky). autor pokryva BDD(rspec), integracne testy(cucumber), stubs, mocks, testovanie webservices, testovanie javascriptu … a to vsetko pri vytvarani realnej aplikacie, ziadna referencna priucka alebo trivialne priklady.
To jsi tady spatne, pro mistni ctenare je ted aktualni zhava novinka unit testing, behaviour nekdy za 3 roky :-)
„A modří už vědí – úplně…“ – já nejsem modrý a stejně vím :)
asi nějakej vegetarián
Neviem si pomoct, ale nejako neviem tomu testovaniu prist na chut…obzvlast pri vytvarani webovych informacnych systemov. Priliz zlozite, zabera to kopec casu a nedokaze to odhalit vsetky chyby. Nebolo by lepsie zaplatit si alfa/beta testerov?
Ja jsem toho nazoru, ze unit testy velmi dobre patrej k bussiness logice, ale hnat se bez zamysleni za 100% coverage je u webovejch vetsi casto kontraproduktivni.
ak pouzivas jazyk ktory nie je dostatocne expresivny a fw ktory nemysli velmi na testovatelnost tak to bude tazke. cital som studiu specificky pre rails, vyvoj pri tdd projektoch bol zhruba o 30% dlhsi, ale s o 90% mensim poctom bugov. pri tdd dostanes ako bonus vyrazne lepsiu architekturu kodu, pretoze ak je testovatelny je lahsie modifikovatelny a rozsiritelny a osobne si myslim ze v dlhodobom horizonte sa ten cas na vyvoj este skrati, pretoze ked si zoberiem refaktoring(a hlavne pri dynamickom jazyku) tak neustale preklikavanie vsetkeho co som mohol nejakou zmenou rozbit vs spustenie testov…
„Our experiences and distilled lessons learned, all point to the fact that TDD seems to be applicable in various domains and can significantly reduce the defect density of developed software without significant productivity reduction of the development team.“ zdroj
Mě tu pořád ještě nikdo nevysvětlil, co to znamená „fw, který myslí/nemyslí na testovtelnost“…
Jasně, pokud budu pracovat ve fw, kterému jenom těžko předám nějaké fake závislosti, tak budou části programu, na které nebudu moc napsat unit testy. Ale tohohle bude ta menší část zdrojového kódu. Zbytek může být normálně otestovaný a fw tomu absolutně nevadí. A ten zbytek se otestuje třeba pomocí integračních Seleniových testů.
Jak už tady zaznělo, kouzlo je mimo jiné v tom, že testování nás nutí výrazně zlepšit architekturu. Nicméně je tu i ekonomické hledisko. To, že testy zaberou nějaký čas je jasný. Otázka je, jestli by ten samý čas nezabralo opakované mačkání F5 a pracné hledání těch základních bugů. A potom znovu, když se v programu něco změní. A pak znovu, když se udělá další úprava. A pak znovu… Zatímco test jednou napíšu a pak ho můžu spouštět v řádově kratším čase a řádově častěji než kdybych testoval ručně. Nedovedu si představit, kolik testerů by bylo potřeba, aby provedli komplexní testování systému za třeba 20 minut. A udělali to po každém commitu :-) Ta investice se nakonec opravdu vrátí!
Sorry, ale tady mluvíme o PHP. Jazyku na vytváření webových stránek. Když dělám webovou aplikaci, tak klienta zpravidla nejvíc zajímá, jestli se správně zobrazuje ve všech prohlížečích, a jestli to opravdu dělá to co má je obvykle až na druhém místě. Jak uděláš automatizované testy na to, jestli se stránky zobrazují ve všech prohlížečích správně? Jak vůbec nadefinuješ, co je správně?
Neříkám, že to nejde, ale vložená námaha by mnohonásobně převážila to močkání F5, i kdybych ho měl dělat několikrát. Nebo snad existuje nějaký software na testování renderování stránek? Dozvíme se o něm v tomhle seriálu? To jsou věci, které mne na testování PHP aplikací zajímají – to co jsme se dozvěděli doteď je poměrně triviální a omezeně použitelné.
Já osobně PHP aplikace automatizovaně netestuju a dosavadní části seriálu mne nepřesvědčily, abych začal. Podle toho, co jsme se zatím dozvěděli, se tenhle způsob testování hodí na serverové aplikace, které mají jasně definovaný vstup a výstup a dá se deterministicky verifikovat, jestli správnému vstupu odpovídá správný výstup. Optimální pro věci typu DBMS, různé mailservery a podobné „démony“. Pro software typu webových stránek (což je kolik procent věcí napsaných v PHP? že by 100?) naprosto nevhodné. Obecně asi pro cokoliv s UI.
Myslím, že většina mluví o PHP, jazyku na tvorbu webových aplikací 8-) Což je důvod, proč nerozumíte. Ono u webových aplikací je jedno, zda se přesně správně zobrazují ve všech prohlížečích (protože pokud ne, oprava bývá ve většině případů snadná), ovšem pokud je problém v chování aplikace, může to mít v tom horším případě neblahý dopad na osud celé softwarové dodávky.
Renderování částečně testovat lze (služby které automaticky provedou screenshoty webu a ty pak porovnávají), ale to už nijak nespadá pod PHP a ani do tohodle seriálu (každopádně to ponechávám volbě autora).
Vtipné slovíčkaření. Když to máte tak jasně rozdělené, tak kde je podle vás ta hranice mezi „stránkami“ a „aplikací“? A dělal jste už opravdu nějakou aplikaci pro někoho za peníze, nebo to máte jen teoreticky?
Pokud dělám eshop s tisícem funkcí nebo sebesofistikovanější intranetový systém na řízení procesů ve firmě, tak se klient stejně vždycky jako první ptá na to, zda se správně zobrazuje, ať už jsou to položky v nabídce eshopu pro zákazníky nebo cokoliv jiného k čemu stránky (aplikace) slouží.
Ano, jsou věci, u kterých je v principu jedno, jestli se UI renderuje přes prohlížeč nebo přes nějaké jiné rozhraní nebo UI v běžné podobě vůbec nemají a které už rozhodně nejsou „stránkami“, ale řekněme třeba síťovým informačním systémem. To předpokládám spadá do vaší definice „aplikace“. Takové věci se pak určitě budou touto metodou testování testovat dobře. Ale jako sorry, ale něco takového přece nebudu psát v PHP. Pak mi bude strašně překážet volné typování a další skvělé featury, které se pro běžné webové stránky v PHP tak hodí.
Než začneš někoho obviňovat z amatérismu nebo z „pouze teoretické“ znalosti problematiky, tak si PROSÍM nejprve svůj výblitek aspoň třikrát přečti. Pokud totiž porovnáš tvůj příspěvěk a Martinův, na který jsi reagoval, tak je nad slunce jasné kdo tu jen machruje… Á propo – co kdybys nás potěšil nějakým opravdu odborným článkem, třeba na téma „Síťové informační systémy“ nebo „Výhody volného typování při vývoji běžných webovývh stránek“? Něco takového bych se opravdu rád přiučil, „volné typování“ bude asi nějaká revoluční novinka, protože PHP zatím patří jen mezi dynamic-type jazyky.
Nikoho jsem neobviňoval, pouze jsem se zeptal, zda už něco v praxi programoval, zvlášť v situaci, kdy přispěvatel není autorem původního článku, od kterého bych nějakou praxi tak nějak očekával. Ohledně české terminologie se celkem nemám chuť dohadovat, pokud není volné typování ten správný překlad „loosely typed language“, který používáte vy, tak sorry, ale předpokládám, že jste pochopil, co jsem tím myslel, takže to asi zas až takový problém nebude.
Pokud příspěvek někoho urazil, tak se omlouvám (nevím, jestli je třeba Martin Hassman místní PHP guru a je urážkou zeptat se ho, jestli už něco programoval za peníze), každopádně nemusíte zacházet k osobním invektivám, jen jsem se snažil dopátrat konstruktivní diskuzí toho, proč je testování PHP aplikací popsaným způsobem rozumné. Možná ale předbíhám další části seriálu, takže si prostě jen počkám a uvidím. Zatím to má ale celkem sestupnou tendenci, tahle část byla (jak už někdo výše poznamenal) spíš něco jako copy-paste z příručky.
Na hodnocení trendu je zatím u seriálu brzy. Tenhle díl byl v seriálu poměrně logickým krokem. Příručky jsou také potřebné. Je vidět, že to některým čtenářům nevyhovuje – nevadí, autor to vezme v úvahu.
Pevná hranice mezi stránkou a aplikací není, v tomhle případě jde hlavně o pohled. Je pro vás v e-shopu „přidání do košíku“ nějakým barevným tlačítkem na stránce nebo funkcí v kódu, kterou lze volat a testovat nezávisle na GUI?
Klient pochopitelně nejdřív reaguje na tu vizuální složku (protože tu na rozdíl od těch hlubších věcí vidí a také ji rozumí – nebo aspoň má ten pocit), ale zatímco vyřešit designerský problém bude zpravidla úkol jednoduchý až triviální a jeho náročnost jeho opravy neporoste s časem, tak oprava chyby v aplikaci se pohybuje na celé škále od triviální po „A neměli bychom to radši celé přepsat?“, „Jakou máme ve smlouvě pokutu?“, „Takže letenku na Bahamy a pryč?“ a navíc s jejím pozdějším odhalením můžou růst náklady a to výrazně.
Srovnejte si vedle sebe problémy, na které se přišlo několik dní od předání a spuštění projektu:
– ve vašem e-shopu kvůli nějakému problému s HTML/JS/CSS nešlo nakupovat v IE
– váš e-shop špatně ukládal adresy zákazníků a nakoupené zboží jste rozesílali do opačných koutů republiky
Z čeho hrozí větší malér? Co víc klienta naštve? Kde vzniknou větší náklady? Co bude snazší napravit?
Díky za odpověď. Máte pravdu ohledně testovatelnosti – přidání do košíku opravdu testovatelné je. A taky máte pravdu, že větší problém je posílat zboží na špatné adresy než špatné zobrazování v IE.
Ale na druhou stranu testovatelné jsou rutiny typu „přidání do košíku“, které jsou tak triviální, že testování víceméně nepotřebují. Složitější akce v softwaru typu eshopu zase vyžadují netriviální vstup uživatele, takže se automatizovaně testují špatně. Chyby typu zasílání na špatné adresy jsou opravdu nepříjemné, ale obvykle vznikají tak, že je testy stejně neodhalí – například změnou sazby DPH z 10% na 14% (pokud vás možnost změny sazby DPH nenapadne při návrhu software, tak ji sebelepší test nevymyslí) nebo prostě chybným zadáním od zákazníka (aha, samozřejmě že jsme mysleli, že „adresa“ znamená „fakturační adresa“ a ne „zasílací adresa“, sice jsme to možná řekli obráceně, ale je přece jasné, že jinak to být nemůže, copak vy jste nikdy neprodával dětské oblečení, člověče?)
Čili závěr – chápu, že to byl jen příklad a že v určitých konkrétních případech se testy mohou hodit, akorát těch případů v běžné praxi moc nevidím.
V tom případně se opravdu neshodneme už onom prvotním předpokladu ‚rutiny typu „přidání do košíku“, které jsou tak triviální, že testování víceméně nepotřebují‘ (vytvořit pro tenhle případ test bude triviální, během vývoje aplikace se může funkčnost takovéhle základní činnosti opakovaně rozbít => nízké náklady na testování => pravděpodobně se vyplatí).
Ani v tom, že „chyby typu špatné uložení adresy testy stejně neodhalí“ – právě v tomhle případě jsou testy poměrně silné (máme daný vstup/vstupy a můžeme snadno zkontrolovat, zda s nimi aplikace provedla to, co měla).
Můžete dát příklad nějaké té vaší „složitější akce v e-shopu“?
Ok, beru argument, že na otestování triviální rutiny stačí triviální test. Je fakt, že tím, že nejsem zvyklý testy používat mi připadá obecně psaní jakéhokoliv testu jako komplikace a velký problém, je ale jasné, že kdybych si na to zvykl, tak napsání triviálního testu bude stejně snadné jako napsání triviální rutiny.
Složitější příklad z praxe mne teď nenapadá a vymýšlet nějaký virtuální asi není potřeba. Počkáme si na další díly seriálu, jestli se dozvíme jak vymýšlet situace, které je potřeba otestovat a jak vlastně ty testy psát.
A ještě k tomu „co bude snazší napravit“. Opravit design bude zpravidla podstatně pracnější.
Jednak proto, že udělat design tak, aby fungoval ve všech prohlížečích od IE6 až pod nejnovější Chrome je prostě těžké – vyžaduje to opakované testování v různých prohlížečích, protože zejména starší verze IE se chovají opravdu nevyzpytatelně.
A jednak proto, že pokud se posílá zboží špatně a je to způsobené chybou programování, kterou mohou odhalit testy, znamená to zpravidla nějakou triviální programátorskou chybu typu použití fakturační adresy místo dodací nebo něco podobného, což se zpravidla opraví snadno. Pokud je to opravdu závažná chyba, že se například vůbec nerozlišuje mezi adresou fakturační a dodací, tak to bývá už chyba návrhu nebo dokonce zadání, a tu vám stejně automatizované testy neodhalí, protože se píší právě podle toho návrhu.
Když už jsme u těch komerčních aplikací – třeba současná verze Ulož.to byla napsána TDD stylem.
Automatizované (unit) testy slouží zejména k odhalení jestli se něco změnilo – například kolega upraví objekt, který já někde používám. Místo aby se on hrabal v mém kódu, jednoduše spustí testy a dozvíme se, které části změna ovlivnila. Je změna objektu triviální programátorská chyba?
K tomu zobrazování – my třeba píšeme aplikaci, pro kterou se počítá jenom se zobrazováním v Chrome/Chromium – ostatní prohlížeče nás nezajímají. To – jestli reaguje/nereaguje JS nebo na stránce je/není prvek už testuje např. Selenium.
Tak tohle je docela zajímavé. Takže vy nemáte domluvené nějaké jasně definované rozhraní, které se buď nemění nebo se při jeho změně příslušným způsobem upraví všechna místa použití, ale místo toho prostě jen upravíte objekt a pak pustíte testy jestli to poběží nebo ne? To mi nepřipadá úplně ok, ale vcelku vám do toho nebudu kecat.
Spíš je na tom zajímavé, že tedy musíte mít jistotu, že testy opravdu pokryjí úplně každou situaci, která může nastat. Je něco takového v reálu možné? Vy máte úplně jasně definované všechny stavy a případy použití, které může uživatel dosáhnout? A máte je pokryté testy? A jak si můžete být jistí, že máte opravdu všechny?
Naopak, obecně bývá definované rozhraní a právě nad ním se spouští testy, které během vývoje ověřují, zda se po provedených změnách kódu rozhraní stále chová tak, jak má. (Pokud testy něco testují, tak to současně i nepřímo definují – už nějaká sepsaná specifikace existuje či nikoliv. Čili testy jdou dohromady k definovanému rozhraní a ne naopak.)
Přesně tak. Navíc jak se aplikace rozšiřuje, může se měnit i rozhraní, které za několik měsíců/let vývoje nemusí odpovídat aktuálním požadavkům (závidím všem co si jednou ve fázi návrhu navrhnou rozhraní, které jim vydrží do ukončení vývoje)
To že testy testují triviální situace je pravda – protože program má být napsaný tak, že jeho malé bloky, testované právě unit testy mají být triviální.
Aha, tak to potom jo. Přišlo mi, že jste psal „kolega upraví objekt, který já někde používám. Místo aby se on hrabal v mém kódu, jednoduše spustí testy“, tzn. místo aby se staral, kde je to použité a jestli tam není potřeba něco změnit, tak prostě jen pustí testy jestli to funguje nebo ne.
Ale přesně takhle to funguje a to je na tom mimojiné super :-D Když vím, že mám dobré pokrytí testy, tak někde něco upravím a spoštěním testů ověřím, jestli to někde něco nerozbilo. Nemusím se nikde v ničem ručně hrabat, selhané testy mi řeknou, co a kde je třeba opravit. Což je prostě boží :-)
Nojo, ale spoléhat jen na automatizované testy, to mi připadá zase druhý extrém, než netestovat vůbec. Vy dokážete mít úplnou jistotu, že máte opravdu všechny způsoby použití toho objektu pokryté testy? Dokážete si být jistý, že vám testy odhalí úplně všechny možnosti chyb? To si neumím v reálu představit.
Honzo, obávám se, že Váš pohled na testování software je poněkud zcestný a pod pojmem „testováním“ máte většině Vámi popisovaných případů pouze akceptační testování. Tedy jakési
symbolické „proklikání“ produktu zákazníkem, zda „na první pohled dělá, co má“. Z Vašich vyjádření jsem poněkud zmaten a nedokážu odhadnout čemu se skutečně věnujete. Pokud je to
tvorba webových prezentací, tedy ve většině případů příprava šablon pro nějaký framework, pak chápu, že Vám toto téma je na míle vzdálené. V takovém případě Vám ani moc možností, kromě
funkčního a akceptačního testování, nezbývá. Funkčními testy si např. ověříte existenci klíčových elementů na stránce (očekávané nadpisy, očekávané odkazy, …), akceptačními
testy si ověříte vzhled. Případně ještě využijete nějaký generátor screenshotů abyste si ověřil vzhled ve Vámi podporovaných prohlížečích.
Jakmile ale vstoupíme na půdu webových aplikací, tedy projektů, které kromě prostého zobrazování víceméně statického obsahu obsahují i nějakou business logiku, tak už se pohybujeme
v prostředí naprosto jiné složitosti. Čím větší aplikace je, tím méně jsme schopni uhlídat všechny aspekty jejího chování. Takovou aplikací může být například Vámi zmiňovaný eshop.
Pokud nejde o „eshop“ na prodej CD na webu nějaké amatérské kapely, tak je třeba počítat s testováním na všech úrovních. Pomocí unit testů ověřujeme chování jednotlivých tříd v naprosté izolaci,
pomocí integračních testů pak jejich vzájemnou spolupráci, případně funkčnost celého modulu. Až na závěr pak nastupují funkční testy, které ověřují, zda výstup odpovídá očekáváním.
A co je nejdůležitější – tato sada testů nás chrání proti zavlečení nechtěné chyby. Mám na mysli třeba Vámi popisovaný příklad s „jasně definovaným rozhraním“. Neznám horší činnost, než
muset prohrabávat zdrojový kód (mnohdy psán někým jiným) a hledat, kde všude je to „jasně definované rozhraní“ použito. Vsadím se s Vámi, že vždy později narazíte na řadu zapomenutých míst.
Samozřejmě už pozdě. Refaktorovat kód, který je pokrytý testy je proti tomu hračka. Pokud kód pokrytý nemám, raději se do žádné větší změny ani nepouštím.
rutiny typu „přidání do košíku“ jsou tak triviální, že testování víceméně nepotřebují
Nevím na jakých eshopech jste pracoval, přidání produktu do košíku způsobí hned několik stavů v aplikaci (změna počtu produktů v košíku, změna počtu produktů ve skladu, …).
testy neodhalí změnu DPH
Vím, jaká je cena bez DPH a vím, jaká má být cena s DPH a toto testuji. Pokud vím, že má dojít ke změně DPH, tak očekávané hodnoty opravím na ty s novou DPH a po změně musí vše opět souhlasit.
Pokud ne, je výpočet ceny s DPH špatně.
Argument s adresami je úplně zcestný, dobrá sada testů ověří, zda po průchodu celým nákupem uložená data odpovídají předpokladu. Objednané položky odpovídají vloženým do košíku, zadané adresy
jsou shodné s uloženými atd. atp.
Ještě krátce k Vaší poslední reakci: Dokážete si být jistý, že vám testy odhalí úplně všechny možnosti chyb. Testy neslouží k tomu aby dokázaly absenci chyb, ale naopak – aby dokázaly jejich
přítomnost a pomáhaly jim čelit, a hlavně – zamezit jejich opětovnému zavlečení do již opraveného kódu.
Díky za obsažný a zajímavý komentář. Určitě si počkám na zbytek seriálu a třeba postoj k automatizovanému testování změním.
Každopádně moje stávající připomínky asi nejlíp vystihuje ten Vámi zmiňovaný příklad se změnou DPH – jak sám píšete, „pokud nevím, že má dojít ke změně, je výpočet špatně“. Tzn. testy pomohou najít jen chyby proti návrhu aplikace, ale chybnou funkčnost aplikace z důvodu špatného návrhu nebo špatného zadání nijak neodhalí. Spoléhat tedy jen na ně, jak navrhuje Clary a nestarat se o kód, jen o testy, mi připadá špatné.
Obávám se, že mícháte dohromady naprosto neslučitelné pojmy. Tím, na co Vy narážíte, tedy jak otestovat fakt, zda aplikace pamatuje na možnost změny DPH, se zabývá tzv. statické testování, ale to už se pohybujeme na úrovni formálního návrhu aplikace. Statické testování nevyžaduje běh reálné aplikace a pomocí tzv. testovacích scénářů se ověřují právě Vámi uváděné možnosti.
Tento seriál se zabývá dynamickým testováním, tedy testováním reálného kódu v konkrétním jazyce.
Nezapomeňte do nákladů na opravu onoho zasílání připočíst i veškeré poštovné za špatně odeslané zboží, komunikaci se zákazníky, ztrátu důvěry (a tím některých zákazníků) apod.
Jak je v porovnání s tím oprava nějaké chyby v designu, která by dobrému designerovi neměla dát problém? Dělat weby pro různé prohlížeče (IE6 u nových zakázek krom výjimečných případů dnes už můžeme vynechat) dnes už není těžké. Jejich chování je poměrně dobře zmapované, tipy a triky popsané, případně existují nástroje, které nás od některých odlišností odstíní. Pokud tohle dnes dělá designerovi problém, bude nejlepší mu doporučit nějaké školení (to nemá být urážka, ale dobrá rada).
myslim že už bys měl radši mlčet a jít pleskat svoje webíky a hlavně si je dokola proklikávat, my ostatní necháme makat procesor a pudem na kafe
To bylo zbytečně kruté ;-)
No, zatím mne nic, co bylo řečeno, nepřesvědčilo, že by automatizované testování bylo právě na ty webíky tak úžasně vhodné. Malý webík radši dvakrát proklíkám, než strávit dva dny psaním testů. Počkáme si na zbytek seriálu.
„Malý webík“ ano. Ale pod tím já si představuju statický web o 4 stránkách a jednom kontaktním formuláři :-) Jakmile je web nasazený na nějakém systému typu CMS, tak tam už se vyplatí mít tento systém otestovaný. A to bez ohledu na to, jak velké weby se v něm dělají.
Ja bejt autorem, tak to ted mozna zabalim :-D
Autor ví, že se nemá nechat nějakými výstřelky odradit. Na tenhle seriál máme obecně dobré odezvy, takže v něm určitě vydržíme.
Autor k tomu nevidí žádný důvod, Vy ano? :-)
Díky za dobrý seriál, já osobně jsem za něj rád.
U tohoto článku by se mi líblo nějaké vizuální zdůraznění textů, které nejsou v oficiální dokumentaci, nebo zmíněných odlišností. MYslíte že by to šlo doplnit?
Omlouvam se ale nedalo mi to. Opravdu jsem necekal co se z tehle diskuze dovim :-)