Přejít k navigační liště

Zdroják » PHP » Testování v PHP: asserty a constraints

Testování v PHP: asserty a constraints

Články PHP, Různé

V dnešním díle si podrobně představíme asserty, které PHPUnit nabízí, a zkusíme si napsat vlastní constraint.

Asserty

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 as­sertSame. 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_Con­straint implementuje rozhraníPHPUnit_Framework_Sel­fDescribing, 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.

Komentáře

Subscribe
Upozornit na
guest
43 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
jen reference

tohle je v podstatě kopie referenční příručky :-(

michal

…. ale, tohle jsem vidět nechtěl, to mám v podrobném manuálu

vidya

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.

prezdivka je povinna

To jsi tady spatne, pro mistni ctenare je ted aktualni zhava novinka unit testing, behaviour nekdy za 3 roky :-)

Vojtěch

„A modří už vědí – úplně…“ – já nejsem modrý a stejně vím :)

jos

asi nějakej vegetarián

Rexxar

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?

Jerry12

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.

vidya

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

arron

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ů.

arron

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í!

Honza

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.

Martin Hassman

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).

Honza

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 sebesofistiko­vaně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í.

Petr P.

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.

Honza

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.

Martin Hassman

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.

Martin Hassman

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?

Honza

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.

Martin Hassman

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“?

Honza

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.

Honza

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.

Clary

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.

Honza

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?

Martin Hassman

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.)

Clary

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í.

Honza

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.

arron

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ží :-)

Honza

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.

Honza

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é.

Martin Hassman

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).

jos

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

Clary

To bylo zbytečně kruté ;-)

Honza

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.

arron

„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í.

Michal

Ja bejt autorem, tak to ted mozna zabalim :-D

Martin Hassman

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.

omelkes

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?

Michal

Omlouvam se ale nedalo mi to. Opravdu jsem necekal co se z tehle diskuze dovim :-)

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.