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

Zdroják » Různé » Základy testování v PHPUnitu (fixtures, anotace, generování testů)

Základy testování v PHPUnitu (fixtures, anotace, generování testů)

Články Různé

Minule jsme se podívali na to, jak zapsat jednoduchý test. Dnes si ukážeme, jak testovat databáze a jak si testování ještě víc zjednodušit a ušetřit u toho čas. Poprvé se podíváme na to, že testování má také svou ekonomickou stránku.

Testování databáze

Velmi často narazíme na situaci, kdy pracujeme s databází. Ta již obsahuje nějaká data. Naše třídy musí buď data rozšířit, upravit nebo vymazat. PHPUnit nám umožňuje pro libovolnou databázi, kam se PHP umí připojit pomocí PDO data vymazat a naplnit vzorovými.

Připojení do databáze je jednoduché. Vypůjčím si příklad z dokumentace PHPUnitu:

class DatabaseTest extends PHPUnit_Extensions_Database_TestCase
{
    protected function getConnection()
    {
        $pdo = new PDO('mysql:host=localhost;dbname=testdb', 'root', '');
        return $this->createDefaultDBConnection($pdo, 'testdb');
    }

    protected function getDataSet()
    {
        return $this->createFlatXMLDataSet(dirname(__FILE__).'/_files/bank-account-seed.xml');
    }
}

(Zdroj: http://www.phpu­nit.de/manual/cu­rrent/en/data­base.html)

V příkladu nalezneme několik odchylek:

  • již nedědíme PHPUnit_Frame­work_TestCase, ale PHPUnit_Exten­sions_Database_Tes­tCase
  • v metodě getConnection použijeme objekt PDO, který je přímou součástí PHP 5.1 (pozor, v často nasazovaném 5.1.6, je PDO nekvalitní).
  • getDataSet – zde určíme data pro načtení do databáze.

V příkladu byl použit tzv.flat XML data set. Zápis dat v takovém XML je triviální:

<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
  <nazevTabulky
    nazevSloupce="hodnota"
    nazevDalsihoSloupce="hodnota"
  />
  <nazevJineTabulky />
</dataset>

Jak vidíte, zapsali jsme si do tabulky nazevTabulky jeden řádek s řetězcem hodnota ve sloupcích nazevSloupcenazevDalsihoSloupce.

Možná přemýšlíte, k čemu je element nazevJineTabulky bez atributů. Tento způsobí, že tabulka bude vyprázdněna.

PHPUnit nabízí alternativně ještě více „ukecaný“ formát XML (který je možné validovat proti DTD) a CSV (kdy jeden soubor odpovídá jedné tabulce v databázi).

Testování databáze ve frameworcích

Dále se podíváme na to, jak se testuje databáze v Zend Frameworku, který používá přímo PHPUnit a srovnáme si přístup s testováním databáze v CakePHP a Ruby on Rails.

Zend Framework používá vlastní jmenný prostor Zend_Test_XXX.

Ve vlastním rodiči testů si nadefinujeme tuto metodu:

    protected function setupDatabase($dataSet, $truncateOnly = false)
    {
        $dbOptions = $this->bootstrap->getBootstrap()->getOption('database');
        $schema = $dbOptions['params']['dbname'];
        $connection = new Zend_Test_PHPUnit_Db_Connection(
            $this->bootstrap->getBootstrap()->getResource('db'),
            $schema
        );
        $databaseTester = new Zend_Test_PHPUnit_Db_SimpleTester($connection);
        $databaseFixture =
                    new PHPUnit_Extensions_Database_DataSet_XmlDataSet(
                        $dataSet
                    );
        if ($truncateOnly) {
            $databaseTester->setSetUpOperation(PHPUnit_Extensions_Database_Operation_Factory::DELETE_ALL());
        }
        $databaseTester->setupDatabase($databaseFixture);
    }

Další použití v testu je již jen o zavolání $this->setUpDatabase(...cesta k fixture...);, vše další už si řešíme pomocí vestavěné podpory přímo v PHPUnitu. O něco více vám pomůže Zend při testování controllerů, o tom někdy jindy.

Zajímavé je i testování modelů v CakePHP. Cake používá SimpleTest, který je PHP 4 kompatibilní alternativou pro PHPUnit.

Zde naleznete způsob, jak testovat modely v Ruby on Rails (tedy velmi zkráceně, koho testování v Ruby zajímá víc, toho bych rád odkázal na knihovny RSPec a Cucumber). Dříve zmiňované knihovny se zabývají takzvaným Behaviour Driven Developmentem, který je velmi vhodný i pro testování jiných částí aplikace (například controllerů).

Jak si zjednodušit práci?

Testování nesmí zabrat příliš moc času. Jak bude zmíněno v poslední kapitole, i při testování jde o peníze. A je jasné, že když budeme mít vyhrazeno na psaní testu 2 hodiny na 6 hodin vývoje (jakkoliv já sám zastávám názor, že by to mělo být 50/50, k takovému poměru se lze dopracovat pouze se skutečně vstřícným nadřízeným), chceme za ty 2 hodiny co nejlépe otestovat kód, který napíšeme/napsali jsme během těch 6 hodin. Zároveň musíme být ale nároční a testy by měly skutečně pokrývat celé chování tříd.

Ukažme si několik zkratek:

Anotace

Anotace v PHPUnitu zapisujeme obdobně, jako jsme zvyklí z formátu PHPDoc dokumentace. Tedy do komentáře, jednotlivé anotace uvodíme znakem @. Anotace, které nás zajímají při zvyšování efektivity, jsou @assert a @expectedException. O prvním si povíme v další kapitole, na druhý ihned:

Jistě jste již přemýšleli (pokud jste si dělali cvičení z minulé kapitoly), jak otestovat výjimky. Nakonec jste pravděpodobně došli ke kódu, který vypadá přibližně takto:

... // kod sablony testu
public function testDivide()
{
    try {
        $this->object->divide(1, 0); // na obsahu prvniho parametru nezalezi
    } catch (InvalidArgumentException $exc) {
        $this->assertEquals(Calculator::ZERO_DIVIDER_CODE, $exc->getCode());
    }
}
... // kod konce sablony

PHPUnit bohužel neumí anotací otestovat číselný kód (v našem případě Calculator::ZE­RO_DIVIDER_CO­DE), nicméně pokud se spokojíme s třídou výjimky a víme-li, že tato metoda může vyhodit pouze tuto výjimku, můžeme si test velmi zjednodušit:

... // kod sablony testu
/**
 * @expectedException InvalidArgumentException
 */
public function testDivide()
{
    $this->object->divide(1, 0);
}

Je škoda, že tato anotace neumí snadno otestovat i kód výjimky, ale třeba v nějaké z budoucích verzí budeme překvapeni.

Generování testů

Druhá anotace assert a další vlastnosti PHPUnitu, o kterých si budeme teď povídat, vám ušetří spoustu času.

Anotace assert pokrývá případ, kdy nám stačí testovat pouze návratové hodnoty (třída nemá žádný side-effect, nebo ho nabízí prostřednictvím 3.třídy, která je již pokryta jiným testem). Zároveň se dá použít pro případ, kdy si testování side effectů dopíšeme do vygenerované třídy sami.

Změnou je to, že nyní anotaci nepíšeme do testu, ale přímo do testovaného objektu. Zapisuje se ve formátu @assert(obsah parametrů oddělený čárkami)operátorvýsledek (například: @assert(1, 2)==3). Operátorů je celá řada a dají se nalézt v dokumentaci (příklad 17.2). Výhodou je tento postup v případě, kdy si například nadefinujeme prázdnou třídu, navkládáme si do anotací obsahy uvnitř, vně definiční obor testované metody a pak speciální hodnoty. Potom si necháme vygenerovat test (bude zmíněno již za momentík). PHPUnit nám vygeneruje šablonu testu, kterou já nepovažuji za úplně šťastnou, nicméně zkuste sami, je to věc individuálního pohledu.

PHPUnit nám umožňuje vygenerovat test z třídy, nebo třídu z testu. První varianta se dá kombinovat s použitím anotace assert. Test je vytvořen pro všechny veřejné metody objektu. Pokud chceme testovat i settery (které nejsou vždy trivální), v podstatě jsme si psaní testu už ušetřili. Obdobně již u dříve zmíněných metod bez side-effectu. Další způsob je, že nejdřív napíšeme test pro objekt, který ještě neexistuje (jeden z přístupů k Test Driven Developmentu), PHPUnit si proskenuje test a najde všechna volání veřejných metod objektu a podle toho vytvoří jeho šablonu. Příkazy pro generování jsou:


phpunit --skeleton-test NazevTridy (uvadi se bez .php) - vygeneruje test ze souboru s třídou NazevTridy v souboru NazevTridy.test
phpunit --skeleton-class NazevTridyTest (uvadi se bez .php) - vygneeruje třídu ze souboru testu

Aby nebylo všechno růžové, pro případy Zend Frameworku a Solar Frameworku je tento postup nepoužitelný, protože štábní kultura zápisu názvů tříd souvisí s adresářovou strukturou a v zásadě se třída jen málokdy jmenuje stejně jako soubor (obvykle model, controller, formulář).

Kdy pokračovat v psaní testů? Jak přesvědčit management k testování?

Je samozřejmé, že i přes velkou snahu autora PHPUnitu o maximální zjednodušení vždy můžou nastat situace, kdy je napsání testu složitější. Již v tomto díle jsme si povídali o složitějších partiích. To je přitom pořád ještě naprostý začátek. Samotné testování (nejen unit testování) vyžaduje zvýšení kompetencí vývojáře. To se promítne do času na naučení postupů a v ideálním případě i v možnosti refaktorovat existující kód do stavu, kdy je možné ho vůbec testovat. To je fixní náklad na začátku (společně s nastavením prostředí).

Zároveň s fixním nákladem na začátku máme i variabilní náklady při vývoji. V některých případech doopravdy důkladné testování vede k rychlejšímu napsání kódu, v jiných případech naopak test může znásobit čas nutný k napsání třídy. U každého to bude asi odlišné, já bych řekl, že u normálních případů mi trvá napsání kódu asi o deset procent více, u složitých cca o 20 procent (mimo opravdu speciálních případů, kdy napsání testu trvá třeba čtyřikrát tak dlouho, než napsání kódu). Vcelku (což je individuální) trávím ale testováním polovinu času vývoje (kvůli tomu, že přibývá plánování testování, spouštění testů, opravy kódu, který rozbije test, vzácně i úpravy testů při změnách kódu).

Teď je otázkou, proč by měl management dovolit psaní testů, když to zabere více času. Těch důvodů je několik, uvedeme si dva klíčové, o ekonomičtějších důvodech proč testovat? si povíme na školení.

Důvod 1: náklady na údržbu

Především lze říci, že dlouhodobě udržovaný kód bývá obvykle na mnohem (ale neskutečně moc) vyšší úrovni, než kód, který „nějak funguje“. Třídy jsou menší, starají se o své zodpovědnosti, tyto jsou otestované. V zásadě se dá říci, že v případě, kdy píšeme testovatelný kód, náš kód je doopravdy odlišný. Především se zjednodušuje místo lokalizace chyb, kód se zjednodušuje, mnohdy i zkracuje, programuje se to, co je skutečně potřeba.

Důvod 2: marketing

Odevzdáme-li aplikaci zákazníkovi a on když si ji prokliká, předpokládá se, že v kódu nejen že není chyba, která by mu znemožnila používat program, čeká se, že žádný zaměstnanec nebude mít kvůli chybě prostoje, že kód nezničí zákazníkova data, že aplikace bude odpovídat svižně. Navíc očekává, že staré chyby se nevrátí. Dodržení těchto požadavků je otázkou dobrého jména firmy, otázkou toho, zda bude firmě klient důvěřovat nebo ne. Pokrytí testy, kvalita, to jsou hodnoty, kterým je možné se přihlásit a které lze využít při propagaci firmy.

Tato série slouží jako základní materiál pro školení testování, které pořádá Internet Info spolu s autorem článku Jiřím Kneslem. Mimo školení testování nabízí Jiří Knesl i školení Zend Frameworku, agilních technik a metodik.

Komentáře

Subscribe
Upozornit na
guest
1 Komentář
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Karel Minařík

Chtěl bych doplnit, jak jinak, odstavec o testování v Ruby On Rails, protože je zavádějící natolik, až hraničí s uváděním v omyl.

> Zde naleznete způsob, jak testovat modely v Ruby on Rails
Odkazovaný guide se zabývá testování celého „stacku“, tedy i integračními testy, „falešným“ voláním controllerů aplikace, apod. Navíc paradoxně není nejlepším úvodem do testování v Rails. Ten naleznete v knize „Agile Web Development With Rails“ nebo „The RSpec Book“.

> (tedy velmi zkráceně, koho testování v Ruby zajímá víc, toho bych rád odkázal na knihovny RSPec a Cucumber).
Nikoliv zkráceně. Zmiňovaný guide je naopak velmi přehledovým materiálem o testování v Rails. Připomenu, že v Rails je podpora pro testy „zapečená“ do té míry, že generátor nového modelu či „resource“ vygeneruje i základ pro testy.
RSpec s Rails nesouvisí, jedná se o obecnou knihovnu pro deklarativní psaní testů v Ruby. Jedná se o alternativu k Test::Unit jakožto „old school“ variantě xUnit původně z Javy.
Cucumber je zase obecný framework pro testování zejména webových aplikací, který umožňuje psát testy přirozeným jazykem, jako specifikaci. Klade stejně jako RSpec důraz na hodnotu pro zákazníka, nikoliv na technické zajištění funkce modulu (jako tradiční jednotkové testování).

> Dříve zmiňované knihovny se zabývají takzvaným Behaviour Driven Developmentem, který je velmi vhodný i pro testování jiných částí aplikace (například controllerů).
To je ale přece nesmírně zavádějící. V Ruby On Rails je standardně k RESTful resource (model+contro­ller+views+rou­tes) vygenerován test pokrývající CRUD funkcionalitu včetně fixtures. Nic dalšího k tomu nepotřebujete.
Behavior Driven Development (BDD) je zase „vhodný“ pro testování čehokoliv v aplikaci, neboť oproti jednotkovému testování jeho smysl nespočívá v technice implementace nebo „hezčím slovním popisu“ ale v důrazu na business value a vyšší míru abstrakce: testuji chování, nikoliv surovou funkcionalitu stylem „input/output“. To samé lze (a je třeba!) samozřejmě ale dělat i v unit testech.
Testování controllerů pokrývá v Rails ActionController::TestCase , nebo např. Rack::Test .
Cucumber zase nesouvisí s Rails a do jisté míry ani s Ruby: je to poměrně agnostický nástroj, který umožňuje psát „spustitelnou specifikaci“ typu When I fill 'something' for 'search' And I click 'Search' Then I should see 'Something found' zejména pro webové aplikace. Jako driver může samozřejmě sloužit „zfalšovaný“ Rails/Rack stack, ale taky Mechanize, Selenium, Watir (automatizace browserů), nebo headless browser typu HtmlUnit apod. Samozřejmě, že v Rails světě je Cucumber nejpoužívanější, ale není na Rails ani na Ruby nijak vázaný. Implementaci testů můžete psát klidně v Pythonu.
Zájemcům maximálně doporučuji video „BDD With Cucumber“, vysvětlující motivaci i konkrétní použití.

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.