Blog: Classier Twitter threads - Tag Testování

Testování konzistence dat

Objevil jsem docela zásadní bug ve svém RedisExtension pro Nette Framework. Poslední vyhrává. Zkuste si spustit 4 ajaxové requesty v jeden moment, které upravují session. Ty první 3 jako by nebyly, protože ten poslední všechny změny přepíše. Průser že? Co s tím?

Zamykání klíče v Redisu

Oficální dokumentace doporučuje použít SETNX. Je úplně jedno jakou datovou strukturu použijete. Redis prostě neblokuje a můžete si klidně dělat stojky na hlavě. Uspávat aplikaci a hádat se o zámek spamováním Redisu? Až jako poslední možnost.

Ovšem po zbytek článku (až ke komentářům) budeme předpokládat, že metodyku zamykání máme vyřešenou. Jak si ale ověříme, že funguje?

Konzistence v PHPUnit

Narovinu, nic takového v PHPUnit nejde. S tím se ale přece nesmíříme!

Nejprve jsem si musel ujasnit, co vlastně chci dělat - chci spouštět kus kódu opakovaně a potřebuji aby se spouštěl ve více vláknech. Strávil jsem půl dne hledáním řešení v čistém PHPUnit. Annotace, podědění a upravení TestCase, nebo TestSuite. Nic, nikde na internetu ani ťuk, ale možná jen neumím hledat?

Vlastní řešení

Nejrozuměji se to dá docílit nějak tak, jak to dělá Nette\Tester. Takže jsem se drobátko inspiroval a vypůjčil si dvě třídy. V mém případě to jsou Process a ParallelRunner. Když předám runneru název scriptu, tak mi ho 100x spustí ve 30ti vláknech (není problém navýšit).

Vymyslel jsem si také, že chci aby se to pěkně používalo.

public function testConsistency()
{
    $sessionDir = TEMP_DIR;
    $this->threadStress(function () use ($sessionDir) {
        session_save_path($sessionDir);
        session_start();
        $_SESSION['counter'] += 1;
    }, 100);
    $this->assertEquals(100, $_SESSION['counter']);
}

Samozřejmě jsem si musel napsat vlastní parser na funkce, protože se nechci spoléhat na přítomnost pcntl rozšíření.

Parsovat funkce a metody je ještě docela hračka, ale zkuste si to s closurama ;)

Tokenizerem projdu soubor a najdu všechny definice funkcí a metod a pomocí ReflectionFunctionAbstract::getStartLine() dokážu spárovat i closury (pokud jich není víc na jednom řádku).

Obsah closury vykopíruju a pomocí reflexe si přečtu hodnoty proměnných předaných přes use(). Přidám bootstrap z testů a nějaké use tříd na začátek souboru a všechno slepím do jednoho scriptu. Takový script už se dá krásně spouštět pomocí ParallelRunner.

Výsledek je funkční testování kódu v desítkách vláken s elegantním zápisem. Jenom to trochu žere (nečekaně) a už se to nedá považovat za unit test ;)

Na závěr zpět k Redisu

Nevíte někdo, co s těmi zámky? Dočasně jsem úplně vypnul RedisSessionHandler, protože je teď tak trochu k ničemu. Pokud na nic nepříjdu, asi přistoupím na řešení s SETNX, ale moc se mi do toho nechce.

Continue reading ...

Začněte testovat

Už je to pár měsíců, co jsem začal testovat a přišel jsem za tu dobu na pár věcí. Především, psát testy se vyplatí. Začal jsem sice poněkud zmateně, ale to dnes snad napravím. Článek je souhrnem poznatků z různých koutků testování a pevně věřím, že Vás nadchne pro jejich další studium.

Instalace konfigurace a konvence

PHPUnit doporučuji nainstalovat pomocí Pearu. Osobně používám 3.6.0RC4 a není s ní problém.

$ pear config-set auto_discover 1
# $ pear install --alldeps --force pear.phpunit.de/PHPUnit-3.6.0RC4 nainstaluje i Symfony YAML reader a pár dalších malých knihovniček

Mám konvenci, že v projektu je složka libs/ a tests/. Když vytvářím nějaký test, tak ho umístím přesně do stejné složky, jako je v libs a suffixnu “Test”, takže třeba tests/Kdyby/Application/PresenterFactoryTest.php a namespace testu je pak Kdyby\Testing\Application. Tato část je velice individuální pro spoustu lidí.

Ve složce s testy je potřebný config a boostrap. Config se jmenuje phpunit.xml a když napíšu ve složce s testy do příkazové řádky $ phpunit , tak si ho PHPUnit automaticky načte. Mně v současné době vyhovuje tato velice standardní konfigurace. Naprostým základem konfigurace, je zapnutí barviček. Co si budeme nalhávat, koho by to bez té zelené bavilo?

Krásně se to používá, když člověk chce pouštět jeden test pořád dokola, aby ho nezdržovaly ostatní testy. Stačí se přesunout do složky s testy a zavolat

$ phpunit Kdyby/Application/PresenterFactoryTest.php

Viděl jsem totiž, že někteří načítají boostrap v každém jednom testu, což je zbytečné a já jsem to taky tak kdysi dělal. Mnohem jednodušší a spolehlivější je pouštět testy z jedné složky tests/, kde je konfigurace a je v ní napsané, kde je boostrap soubor.

Tím se dostáváme k boostrap.php, což je soubor, ve kterém je nutné nastavit autoloading tříd, popř. provést základní konfiguraci prostředí. Spustí se před začátkem testů a musí se zkratká postarat, aby jim nic nechybělo.

Tyto dva soubory si můžeme nastavit do IDE a pouštět testy z něj. V NetBeans toto nastavení vypadá například takto:

netbeans-phpunit-setup

A průběh testů vypadá takto:

netbeans-phpunit-runing

Hello world test

Máme nainstalovaný a nastavený PHPUnit a můžeme napsat první test.

class MyHelloWorldTest extends PHPUnit_Framework_TestCase
{
    public function testOneEqualsOne()
    {
        $this->assertTrue(1 == 1);
    }

    public function testHelloEqualsHello()
    {
        $this->assertTrue("hello" == "hello");
    }
}

Všimněte si hlavně pojmenování. Názvy třídy i metod jsou jako věta a popisují to, co se v testu děje a k čemu se vztahuje. V testech pak voláme tzv. “asserty”. Vyjadřují podmínku, jakou jejich argumenty musí splnit. PHPUnit z toho pak generuje přehled a řekne nám, když některé testy neprojdou a proč neprošly. Je to takový chytřejší automatický dump() se statistikami.

Assertů je celá řada a doporučuji si je projít všechny. Oficiální dokumentace obsahuje pěkné a názorné ukázky.

Test si tedy uložíme, třeba do souboru tests/Kdyby/MyHelloWorldTest.php a spustíme

phpunit-green

Krásná zelená, testy fungují a můžeme začít vyvíjet!

Chytřejší unit testy

PHPUnit nabízí možnost, překrýt si, mimo jiné, metody setup a teardown. Tyhle se opakovaně volají před každým zavoláním testovací metody a po každém zavolání testovací metody.

Ale pozor, je tu jeden chyták. PHPUnit vytváří pro každé zavolání testovací metody nový objekt testu. Není proto možné sdílet nějakou proměnnou mezi dvěma testy. Ovšem občas je to potřeba a na to se používá annotace @depends. Krásně to jde pochopit z ukázky v dokumentaci.

Dalším šikovným nástrojem jsou “zdroje dat”. Často je potřeba pouštět ten samý test pro více různých vstupů a výstupů a bylo by velice otravné vypisovat jednotlivé asserty jen s různými proměnnými. Opět je to velice pěkně ukázané na příkladu v dokumentaci.

Co je asi nejdůležitější a velice opomíjená věc, je testovat chybové stavy. Je velice důležité mít otestované, že se třída nebude chovat nepředvídatelně v neočekávaných stavech, ale že třeba vyhodí výjimku. Na to se hodí testování výjimek.

Testování databáze

Tohle je perlička sama o sobě. PHPUnit nabízí brášku jménem DbUnit, který se tváří jako solidní základ pro testování databáze. Když pominu fakt, že není kompatibilní s posledním PHPUnitem, ale “jen” s 3.5, tak je to celkem použitelný nástroj. Má to ovšem několik ale, které jsem já nepřekousl:

  • Zabere si metody setup a teardown, které když chcete použít, nesmíte zapomenout volat předka parent::setup() a parent::teardown() a nijak nás neupozorní, když zapomeneme (upozorní nás až nefunkční test a nesouvisející hlášky)
  • DataSet je jakýsi objekt, do kterého se vkládají další objekty, pro jednotlivé tabulky, které obsahují jednotlivé řádky tabulek. Tyto DataSety se pak porovnávají. Nejenom, že mají opravdu hloupé API, ale dokonce mají i otřesně řešené asserty. Představte si, že máme tabulku se stovkami záznamů a testujeme dva DataSety. PHPUnit nám správně řekne, že se třeba nerovnají, ale zároveň do toho pomocí “ASCII grafiky” vypíše jednotlivé záznamy a to tak, že úplně všechny. Běžný smrtelník nemá šanci na prví pohled najít rozdíl a opravit tak kód.
  • Maže a vytváří databázi úplně pokaždé. Tento bod je technicky vzato správně. Jak jinak docílit dokonalou izolovanost testů, než že se pro každý test vytvoří znovu čisté schéma. Ovšem je tu problém s výkonem, taková operace je logicky tím náročnější, čím více máte tabulek a tím pomalejší. Jako bonus zahazuje spojení s databází a vytváří nové, před úplně každým testem, i když v něm nejsou operace s databází.

Používám na práci s databází Doctrine 2. Jedno jeho rozšíření obsahuje vrstvičku nad DbUnitem, která má za úkol obnovovat databázi a integrovat tyto dva nástroje do sebe. Ani tento však není pro mě dostatečně použitelný. Je to tak na hraní a pochopení, co je kde potřeba pohlídat.

Od DbUnit jsem tedy upustil a vymyslel si vlastní udělátko. Vysvětlím pouze obecný princip, koho by zajímaly detaily, najde je v mém repozitáři na githubu.

Mám poděděný PHPUnit_Framework_TestCase a v něm, ve statické vlastnosti, instanci třídy MemoryDatabaseManager. Tato třída umí na požádání vytvořit nakonfigurované objekty, které potřebuji k práci s databází, u Doctrine je to EntityManager a jeho závislosti. Byť tady porušuji princip izolovanosti, na svou obranu musím říct, že tím získám obrovské zvýšení výkonu hned z několika důvodu.

Celé je to lazy. V momentě prvního požadavku o EntityManager, se vytvoří připojení na SQLite Memory (tento typ databáze na testování doporučuje i PHPUnit v dokumentaci) a dalším krokem je vytvoření schématu databáze. Díky tomu, že si připojení držím staticky, můžu ho recyklovat a vždy jen vyprázdním databázi před dalším testem. Princip izolovanosti tedy porušuji jenom “tak trošku” a získám tím ohromné zvýšení výkonu.

Menší nevýhoda tohoto přístupu je, že si musím psát vlastní assert metody, pokud chci testovat přímo databázi nebo nějaké výsledky operací.

Vývoj řízený testy

“TDD”((Test Driven Development)) říká, že první jsou testy a pak až implementace. Když totiž programátor napíše nejdříve kód, který bude konečnou implementaci používat, tak dovede třídu navrhnout mnohdy lépe, než kdyby strávil hodiny nad papírem, nebo nějakým class diagramem.

TDD také definuje iteraci “red, green, refactor”. Ve zkratce to znamená, že se napíše test a ten se spustí. Testovací nástroj na nás bude křičet červeně, protože test neprojde, nebyl totiž implementován. Dalším krokem je jeho implementace. Napíšeme nezbytné minimum kódu pro to, aby test fungoval. Když se nám objeví zelená, tak refaktorujeme. Zamyslíme se, co by šlo udělat lépe a implementaci měníme k dokonalosti v nekonečné smyčce “red, green, refactor”.

Osobně mám s tímto přístupem problém. Možná se málo snažím, možná jsem ze staré školy, ale psát prvně testy se asi jen tak nenaučím. Pro začátek mi stačí, že mám třídy pokryté testy, i když byly napsány až po implementaci.

Continue reading ...