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

Zdroják » PHP » Testování v PHP: odstiňujeme závislosti II.

Testování v PHP: odstiňujeme závislosti II.

Články PHP, Různé

Je možné mockovat SOAP webservice? A co filesystém? A co když potřebuji otestovat abstraktní třídu? Nejen na tyto otázky vám odpoví další díl seriálu o testování v PHP.

minulém díle našeho seriálu jsme si ukázali základy mockování v PHPUnit. Dnes se podíváme ještě na tři metody, které nám mohou pomoci při řešení více či méně zapeklitých situací. Jednou z nich může být – jak otestovat abstraktní třídu?

Testování/mockování abstraktní třídy

Přesně pro tyto účely framework PHPUnit, přesněji třída PHPUnit_Framework_TestCase, nabízí metodu getMockForAbstractClass. Seznam parametrů metody je „téměř stejný“ se seznamem parametrů metody getMock, i její chování je až na několik, ne příliš dobře zdokumentovaných, detailů „téměř totožné“. Zkrátka ve světě PHP nic neobvyklého :-/.

V čem jsou tedy rozdíly? Druhým parametrem není pole se seznamem mockovaných metod. Tento parametr je přesunut až na sedmé místo. I jeho význam je trošku modifikovaný – umožňuje dodefinovat seznam mockovaných metod, které budou přidány k defaultně mockovaným abstraktním metodám. Při vytváření mocku abstraktní třídy tedy musíte myslet na to, že framework za vás defaultně mockuje všechny abstraktní metody!

Kdy se hodí tato možnost? Potřebujete-li např. otestovat abstraktní třídu, která obsahuje šablonovou metodu.

abstract class Vehicle
{
    public function canFly()
    {
        return $this->hasWings();
    }

    abstract public function hasWings();
}

class VehicleTest extends PHPUnit_Framework_TestCase
{
    public function testCanFly()
    {
        $mock = $this->getMockForAbstractClass("Vehicle");
        $mock->expects($this->once())
            ->method("hasWings")
            ->will($this->returnValue(false));

        $this->assertFalse($mock->canFly());
    }
}

Mockování webservice

Pro odstínění závislosti na webservice, konkrétně instanci třídy SOAPClient, je možné použít metodu getMockFromWsdl, která vygeneruje mock podle specifikace popsané v zadaném WSDL souboru. Osobně nepovažuji tuto metodu za příliš užitečnou, protože vytvořený mock je instancí typu SOAPClient, což nemusí být vždy to, co potřebujeme. Mnohem lepším řešením je abstrakce webservice. K tomuto problému se ještě dostaneme v souvislosti s návrhem testovatelného kódu.

Mockování filesystému

Odstiňování závislostí na filesystému patří mezi jedny z nejsložitějších úkolů při testování. Ne vždy je to totiž dost dobře možné a hodně závisí na konkrétní implementaci testované třídy. Jedním z možných řešení je použití stream wrapperu virtuálního filesystému – vfsStream.

Instalace vfsStream

Instalovat vfsStream podle aktuální dokumentace PHPUnit se ani nepokoušejte, už dávno nefunguje. Projekt byl přesunut. Ale začátkem roku byl projekt publikován na serveru Packagist.org aby bylo možné jej instalovat pomocí nástroje Composer, tak doufejme, že to povede k ustálení instalačního postupu.

Jednou z možností je tedy instalace pomocí Composer, takto je možné instalovat vfsStream od verze 1.0.0. Seznam všech možných verzí je dostupný na serveru Packagist.org. Pro instalaci pomocí Composer stačí vfsStream zařadit mezi dependencies.

    "mikey179/vfsStream": "1.1.*"

Alternativní možností je použití instalátoru PEAR, ale takto lze nainstalovat pouze verzi menší než 1.0.0. Pro aktuálnější verze už je nutné použít předchozí postup.

    $ pear channel-discover pear.bovigo.org
    $ pear install bovigo/vfsStream-beta

Malá ukázka použití – mockování filesystému při testování třídy Logger, jejímž účelem je logování do souboru.

class Logger
{
    protected $directory;

    public function __construct($dir)
    {
        $this->directory = $dir;
    }

    public function log($message)
    {
        $fp = fopen($this->directory."/log.txt", "a");
        fwrite($fp, $message . "n");
        fclose($fp);
    }
}
require_once 'vfsStream/vfsStream.php';

class LoggerTest extends PHPUnit_Framework_TestCase
{
    /**
     * @var  vfsStreamDirectory
     */
    private $root;

    protected function setUp()
    {
        $this->root = vfsStream::setup("logs");
    }

    public function testDirectoryIsCreated()
    {
        $logger = new Logger(vfsStream::url("logs"));
        $logger->log("Log message");
        $logger->log("Next message");

        $logFile = $this->root->getChild("log.txt");
        $this->assertEquals("Log messagenNext messagen", $logFile->getContent());
    }
}

Jak už jsem uváděl výše – použití tohoto postupu závisí vždy na konkrétní implementaci testované třídy a v určitých případech může být tento postup předem vyloučen. Příkladem může být definice cesty k souboru přímo v kódu. Takovou závislost nemáme šanci jakkoli nahradit.

Praktický příklad

Tím jsme dokončili průlet základy mockování v PHPUnit, pojďme si nyní opět zkusit něco z praxe. V diskusích pod jednotlivými díly seriálu se často skloňoval výraz „e-shop“, proto zadání zvolím z této oblasti. I tentokrát budeme postupovat metodou „tests first“. Procvičíme si i schopnost rychlé orientace v cizím kódu :-).

Provozovatel e-shopu by rád automaticky importoval do své databáze produkty podle datových souborů svých dodavatelů. Dodavatelé mají k dispozici různou škálu formátů a specifikací (CSV, XML, TXT), proto bude nutné umět vše správně parsovat. Po úvodní schůzce bylo rozhodnuto, že úkol budou řešit paralelně dva týmy – jeden bude řešit parsery, druhý importní třídu. Také bylo dohodnuto společné rozhraní, které budou implementovat všechny parsery. My jsme členy druhého týmu a našim úkolem je tedy implementace importní třídy pouze se znalostí společného rozhraní parserů a znalostí způsobu ukládání dat do databáze. Bohužel nemáme k dispozici žádnou testovací databázi, žádný ad-hoc test zkušebním importem tedy nepřipadá v úvahu.

Pojďme si projít, co vše máme k dispozici.

1) společné rozhraní parserů

interface ProductParser {

    public function getCode();
    public function getName();
}

2) způsob ukládání dat

Máme k dispozici třídu ProductRepository, říkejme jí třeba repozitář, která nabízí dvě metody: insert() a update(). Obě přijímají jako parametr instanci třídy Product (říkejme jí entita), metoda update() navíc vyžaduje index aktualizovaného produktu.

class Product
{
    private $name;

    public function setName($name)
    {
        $this->name = $name;
    }
}

class ProductRepository
{
    public function insert(Product $product) {/*...*/}

    public function update($index, Product $product) {/*...*/}
}

3) model konverzní tabulky

Stejně jako repozitář k ukládání produktů máme k dispozici i model nad konverzní tabulkou. K čemu to potřebujeme? V konverzní tabulce si udržujeme páry „cizí identifikátor – náš identifikátor“ abychom byli schopni určit, zda importovaný produkt už existuje a potřebuje tedy aktualizaci nebo ještě neexistuje a je třeba jej založit. Konverzní tabulka je realizována třídou ProductConversion, která nám nabízí dvě metody:

  • exists($foreignId) – ověřuje, zda existuje záznam pro cizí identifikátor, tedy zda produkt už existuje. Pokud ano, pak vrací náš identifikátor produktu. V opačném případě bool(false).
  • insert($foreignId, $productId) – ukládá nový pár identifikátorů.
class ProductConversion
{
    public function exists($foreignId) {/*...*/}

    public function insert($foreignId, $productId) {/*...*/}
}

Postup importu

Ještě než se vrhneme do kódu, ujasněme si, jak bude import probíhat.

  1. Konkrétní parser nám vrátí instanci třídy ProductParser, která reprezentuje načtený záznam produktu ze souboru.
  2. Pomocí konverzní tabulky se pokusíme načíst náš identifikátor dotyčného produktu.
  3. Pokud náš identifikátor existuje, aktualizujeme záznam produktu v databázi.
  4. V opačném případě založíme nový produkt a uložíme nový konverzní pár identifikátorů.

Implementace kostry

Vytvoříme si kostru importní třídy. Pojmenujeme ji třeba ProductImport a budeme zatím počítat jen s jednou metodou: import(). Bude mít jeden parametr – instanci parseru produktu, který chceme importovat. Naše třída bude určitě ke své práci potřebovat model konverzní tabulky a repozitář pro práci s databází. Obě závislosti uvedeme v konstruktoru. Víc nás zatím nezajímá.

class ProductImport
{
    /**
     * @var ProductConversion
     */
    private $conversion;

    /**
     * @var ProductRepository
     */
    private $repository;

    public function __construct(ProductRepository $repository,
                                ProductConversion $conversion)
    {
        $this->repository = $repository;
        $this->conversion = $conversion;
    }

    public function import(ProductParser $parser) {}
}

Testovací případ pro založení nového produktu

Nyní se pustíme do prvního test case, tím bude založení nového produktu. Co k tomu bude třeba?

  1. Připravit mock třídy ProductRepository, který bude nahrazovat skutečný repozitář. V případě zakládání nového produktu by měla být právě jednou zavolána jeho metoda insert(). Její metoda update() by neměla být zavolána nikdy.
  2. Připravit mock třídy ProductConversion. V případě zakládání nového produktu by měla být právě jednou zavolána jeho metoda exists(), která má vrátit bool(false) (náš identifikátor produktu zatím neexistuje) a právě jednou metoda insert(), ukládající nový konverzní pár.
  3. S pomocí instancí obou mocků vytvořit instanci třídy ProductImport.
  4. Připravit mock parseru, který vytvoříme proti rozhraní ProductParser. Důležité je mockovat metodu getCode(), která bude předchozími mocky volána. Na zbylých nezáleží.
  5. Připravit instanci entity, kterou budeme importovat. Data k její instancializaci nám během skutečného importu poskytne parser.
  6. Zavolat metodu import() s instancí mocku parseru jako parametrem.

Pojďme dát vše dohromady. Jak poznáme, že byl test splněn? Kromě faktu, že všechny expektace musí být splněny, přidáme metodě import() ještě návratovou hodnotu, jíž bude náš identifikátor importovaného produktu. Na závěr test case jej ověříme.

class ProductImportTest extends PHPUnit_Framework_TestCase
{
    public function testInsertNewProduct()
    {
        $futureNewProductId = 111;
        $foreignProductCode = "PROD01";
        $productName = "Test product";

        $product = new Product();
        $product->setName($productName);

        $repository = $this->getMock("ProductRepository");
        $repository->expects($this->once())
            ->method("insert")
            ->with($product)
            ->will($this->returnValue($futureNewProductId));

        $repository->expects($this->never())->method("update");

        $conversion = $this->getMock("ProductConversion");
        $conversion->expects($this->once())
            ->method("exists")
            ->with($foreignProductCode)
            ->will($this->returnValue(false));

        $conversion->expects($this->once())
            ->method("insert")
            ->with($foreignProductCode, $futureNewProductId);

        $parser = $this->getMock("ProductParser");
        $parser->expects($this->once())
            ->method("getCode")
            ->will($this->returnValue($foreignProductCode));

        $parser->expects($this->once())
            ->method("getName")
            ->will($this->returnValue($productName));


        $import = new ProductImport($repository, $conversion);

        $this->assertEquals($futureNewProductId, $import->import($parser));
    }
}

Možná vás délka test case a „ukecanost“ tvorby mocků zprvu vyděsí, ale je to jen otázka zvyku. Stačí se držet jednoduchého postupu: jaká třída, jak často jaká metoda, s jakými parametry, co má udělat/vrátit. Ale zpět k příkladu. Asi nikoho nepřekvapí, že test selže – null se opravdu nerovná 111, což představuje budoucí identifikátor nově uloženého produktu.

PHPUnit 3.7.0 by Sebastian Bergmann.

F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) ProductImportTest::testInsertNewProduct
Failed asserting that null matches expected 111.

Testovaná metoda import() celkem logicky vrací null, není totiž vůbec implementována. Máme připraven test, zkusme nastřelit její funkčnost.

class ProductImport
{
    /**
     * @var ProductConversion
     */
    private $conversion;

    /**
     * @var ProductRepository
     */
    private $repository;

    public function __construct(ProductRepository $repository, ProductConversion $conversion)
    {
        $this->repository = $repository;
        $this->conversion = $conversion;
    }

    public function import(ProductParser $parser)
    {
        $product = new Product();
        $product->setName($parser->getName());

        $productId = $this->conversion->exists($parser->getCode());
        if (!$productId) {
            // create new product
            $productId = $this->repository->insert($product);
            $this->conversion->insert($parser->getCode(), $productId);
            return $productId;
        }
    }
}

Implementace vypadá dobře, zkusme pustit test.

There was 1 failure:

1) ProductImportTest::testInsertNewProduct
ProductParser::getCode() was not expected to be called more than once.

Oops, máme špatně definovanou expektaci u mockované metody getCode() v třídě ProductParser. V metodě import() ji totiž nevoláme jednou, ale dvakrát – poprvé pro ověření existence konverzního páru a podruhé pro uložení nového páru. Upravíme tedy test aby odpovídal skutečnosti a spustíme znovu.

$parser->expects($this->exactly(2))
    ->method("getCode")
    ->will($this->returnValue($foreignProductCode));PHPUnit 3.7.0 by Sebastian Bergmann.

...

Time: 0 seconds, Memory: 5.75Mb

OK (3 tests, 7 assertions)

Testovací případ pro aktualizaci existujícího produktu

Super! Variantu se založením nového produktu máme hotovou. Naše metoda import() zavolala správné metody se správnými parametry a vrátila očekávaný výsledek. Zbývá tedy doplnit variantu, kdy produkt už existuje a je třeba jej pouze aktualizovat. Opět si napíšeme nejprve test. Co očekáváme:

  • Bude jednou zavolána metoda ProductConversion::exists a tentokrát vrátí int identifikátor.
  • Metoda ProductConversion::insert nebude zavolána nikdy.
  • ProductParser::getCode bude zavoolána pouze jednou – při ověření existence konverzního páru.
  • ProductParser::getName také jednou.
  • Právě jednou bude zavolána metoda ProductRepository::update, se správnými parametry.
  • ProductRepository::insert nebude zavolána nikdy.
  • Metoda import() vrátí identifikátor aktualizovaného produktu.

Pojďme to přepsat do kódu. Pro stručnost uvedu pouze nově přidaný test case:

public function testUpdateProduct()
{
    $existentProductId = 222;
    $foreignProductCode = "PROD02";
    $productName = "Test product 2";

    $product = new Product();
    $product->setName($productName);

    $repository = $this->getMock("ProductRepository");
    $repository->expects($this->once())
        ->method("update")
        ->with($existentProductId, $product)
        ->will($this->returnValue($existentProductId));

    $repository->expects($this->never())->method("insert");

    $conversion = $this->getMock("ProductConversion");
    $conversion->expects($this->once())
        ->method("exists")
        ->with($foreignProductCode)
        ->will($this->returnValue($existentProductId));

    $conversion->expects($this->never())->method("insert");

    $parser = $this->getMock("ProductParser");
    $parser->expects($this->once())
        ->method("getCode")
        ->will($this->returnValue($foreignProductCode));

    $parser->expects($this->once())
        ->method("getName")
        ->will($this->returnValue($productName));


    $import = new ProductImport($repository, $conversion);

    $this->assertEquals($existentProductId, $import->import($parser));
}

Když nyní testy spustíme, tak celkem logicky selžou.

There was 1 failure:

1) ProductImportTest::testUpdateProduct
Failed asserting that null matches expected 222.

Ano, null se opravdu nerovná 222. Metoda import() zatím není na možnost aktualizace existujícího produktu připravena. Hned to napravíme.

public function import(ProductParser $parser)
{
    $product = new Product();
    $product->setName($parser->getName());

    $productId = $this->conversion->exists($parser->getCode());
    if ($productId) {
        // update existent product
        $this->repository->update($productId, $product);
        return $productId;
    } else {
        // create new product
        $productId = $this->repository->insert($product);
        $this->conversion->insert($parser->getCode(), $productId);
        return $productId;
    }
}

Úprava byla triviální – přidali jsme jen volání metody ProductRepository::update a vrátili ID produktu. Zkusme testy nyní.

PHPUnit 3.7.0 by Sebastian Bergmann.

....

Time: 1 second, Memory: 5.75Mb

OK (4 tests, 11 assertions)

Cool! Všechny expektace byly splněny, máme hotovo! Jak vidíte, byli jsme schopni implementovat požadovanou funkčnost i bez existence jakéhokoli konkrétního parseru. Díky tomu jsme mohli úkol řešit paralelně a dokončit jej dříve než při běžném postupu.

Další možnosti mockování

Pokud vás odpuzuje trochu „ukecanější“ postup tvorby mocku v PHPUnit nebo postrádáte další možnosti nastavení, pak můžete sáhnout hned po několika externích knihovnách. Doporučuji prozkoumat například Mockery (https://git­hub.com/padraic/mockery) nebo Mockista (https://bit­bucket.org/jiriknesl/mockis­ta) od Jirky Knesla.

Všechny uvedené zdrojové kódy příkladu jsou dostupné na Githubu:

https://github.com/josefzamrzla/serial-testovani-v-php

Příště

To je pro dnešek vše. Příště se podíváme na rozšíření DbUnit a integrační testy.

Komentáře

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

To je ale pěkné české slovo :-)

andrej.k

a co tak ‚expektace‘ v zavere clanku?

Honza Marek

vfsStream bohužel není použitelný zdaleka vždycky. Například funkce realpath na virtuální soubory z vfsStreamu vždycky vrací false.

Filip Procházka

Jak to, že tu nevidím založenou issue? https://github.com/mikey179/vfsStream/issues

Honza

Líbí se mi, že příklady vkládanými mezi výklad udržujete seriál v kontaktu s realitou a tím zajímavý i pro nedočkavé čtenáře jako jsem já, kteří chtějí „už vidět jak to teda v reálu funguje“ a nechtějí čekat až na konec seriálu.

Co jsem si z dnešního příkladu odnesl? Bohužel to vidím na potvrzení mého dosud poměrně skeptického postoje k automatizovaným testům. Co je v příkladu ukázáno:
– testovali jsem funkci o délce 17 řádek kódu
– udělali jsme na to dva testy, které mají dohromady přes 80 řádek kódu
– testy testují dva případy použití (což by měla být kompletní sada možností použití metody, aby byla pokrytá testy)
– tyto dva testy nijak nezaručily, že metoda bude skutečně fungovat správně!

Je to výborný příklad toho, co jsem už dvakrát zmiňoval v diskuzi, a sice že testy se skutečně otestuje jen to, na co programátor myslel, že se má otestovat, tzn. co ho napadlo, že může být špatně. Pokud programátora opravdu napadne, co všechno se může stát špatně, tak to může rovnou napsat do kódu a ušetří (v tomto případě) 83% kódování. Pokud ho to nenapadne, tak ho testy nezachrání, protože tu chybu stejně neotestuje.

A jaká je tedy i po úspěšném absolvování dvou testů v testované metodě chyba? Neřeší situaci, kdy produkt v repository existuje a má klíč „0“. Dojde k chybě – produkt se vloží nově, protože if ($productId) pro 0 neplatí. Situace s 0 by měla být nějak ošetřena, buď požít === nebo explicitně definovat v popisu interface parseru, že getCode() nesmí 0 vracet.

Honza

Ještě aby to nevypadalo, jako že automatizované testování úplně odmítám, tak to uvedu na pravou míru: ten příklad je dobrý i proto, že jde zrovna o situaci, kdy automatizované testování zjevně smysl má – pokud budu mít několik parserů (nejlépe opravdu hodně), tak se mi rozhodně vyplatí sestavit si testy na otestování, zda funguje parser správně, navíc když je to externí kód vytvářený jiným teamem.
Na druhou stranu na metodu, která je v příkladu, bych asi automatizovaný test nepoužil, protože není součástí parseru, ale typu, který budu zřejmě psát jen jednou. A pokud mne má test ochránit před chybami při změnách v implementaci, tak se musím opravdu zamyslet nad tím, jestli se mi vyplatí psát skoro 6x víc kódu. Jaká je pravděpodobnost, že celou implementaci 5x přepíšu? A že se přitom vůbec nezmění interface, takže budou testy dál použitelné?

A ještě jedna poznámka: testy mohou dávat falešný pocit bezpečí – pokud to někdo udělá jako v tomto případě a začne machrovat „mám testy pokryto 100% use cases kódu“, tak se bude o to víc divit, až někdo napíše repository, které nedává produkty do databáze, ale do PHPkového pole, kde je 0 normální platný klíč.

Čili můj názor je: automatizované testy jsou užitečné v těch případech, na které se hodí, ale mohou pouze pomoci hledat chyby – spoléhat výhradně na ně je hloupost.

arron

Souhlasím naprosto bezvýhradně s jednou věcí. Spoléhat výhradně na unit testy je hloupost :-) Ono totiž asi žádný test nezaručí 100% jistotu, že všechno bude fungovat. My se těmi testy jenom k těm 100% přibližujeme. Odhadem bych řekl, že tímhle testem jsme zvýšili míru jistoty, že ten kód funguje správně z takových 50% (může fungovat, ale nemusí, kdo ví) na takových 95% (v drtívé většině případů funguje). A to za těch 80 řádků kódu docela stojí.

Navíc máme hned velkýho pomocníka. Do vyjádření užitku toho unit testu se totiž musí započítat i to, že se bude provádět dokola velmi často a tím nás ujišťovat o těch 95%. Kolik by stál programátor, který by po každém commitu tenhle kousek kódu testoval ručné?? Opět mi přijde, že těch pár řádek kódu bylo řádově levnějších :-)

Ono ve finále je tenhle test zbytečně dlouhý. Používá pouze nativní phpunit. Když se píše nějaký projekt, tak okolo těch testů stejně vznikne jakýsi „pseudojazyk“ pomocí kterého se pak budou testy psát daleko rychleji a kratšeji. Abych jenom neplácal, tak například:

public function testUpdateProduct()
{
$existentProductId = 222;
$foreignProductCode = "PROD02";
$productName = "Test product 2";

$product = new Product();
$product->setName($pro­ductName);

$this->expectDependen­cyCall(‚Produc­tRepository‘, ‚update‘, array($existen­tProductId, $product), $existentProduc­tId);
$this->expectDependen­cyCall(‚Produc­tConversion‘, ‚exists‘, array($foreig­nProductCode), $existentProduc­tId);
$this->expectDependen­cyCall(‚Produc­tParser‘, ‚getCode‘, NULL, $foreignProduc­tCode);
$this->expectDependen­cyCall(‚Produc­tParser‘, ‚getName‘, NULL, $productName);

$import = new ProductImport($re­pository, $conversion);

$this->assertEquals($e­xistentProduc­tId, $import->import($parser));
}

Hle, hned je to o dost kratší :-) (přiznávám se, že používám poněkdu jinou metodu testování závislostí, ale řekněme, že je to implementační detail a pointa je jasná :-))

Honza

Podobenství k tomu testování každého kousku kódu po každém commitu a placení programátora, který by to dělal:
Zjistili jsme, že abychom měli doma čisté koberce, potřebujeme si je jednou ročně nechat vyčistit speciálním strojem, což stojí asi 1000 kč. Ale manželka přišla na super zlepšovák – ten stroj si koupíme. Stojí to sice 50000 Kč, ale už nebudeme muset platit za čištění firmě. Od té doby čistíme koberce pravidelně každý týden – no a co byste řekli, cena stroje se nám za rok vrátí, protože jinak bychom museli každý týden platit 1000 Kč.

Vysvětlení, pokud to není jasné: cena vyčištění koberců = cena programátora, aby něco otestoval, cena stroje = cena programátora, aby napsal test. Čištění koberce jednou ročně = otestování kódu, zda je v pořádku při jeho vytvoření a při jakýchkoliv operacích s ním. Čištění koberce jednou týdně = automatické testy po každém commitu.
Ale klidně můžeme ještě pokračovat: čištění jednou ročně specializovanou firmou (perfektní vyčištění od profesionála se zárukou) = kvalitní manuální otestování nad kterým se přemýšlí. Čištění týdně svépomocí jak to tak sám umím = automatizovaný test, který najde jen to, na co se myslí při jeho psaní.

Tzn. testovat všechno po každém commitu je zbytečné. Testovat věci, kterých ten commit týká, zbytečné není.
Každopádně ale díky za ukázku alternativního zápisu testu.

EsoRimer.

Skvělý příklad, jen s drobnou chybkou.

V „realitě“ to obyčejně dopadá tak, že čistějí jednou týdně vyjde na 100Kč, 1 po roce na 50000 …

Honza

Tak teď nevím, jestli myslíte testy nebo koberce. Jestil koberce, tak fakt nevím, kdo vám je vyčistí za 100 Kč a těch 50000 je úplný nesmysl. Jestli testy, tak to trochu rozveďte co se čím myslí. Pokud má být těch 100 Kč automatizovaný test, tak ten by měl být „zadarmo“ (pokud nepočítám elektřinu na běh procesoru a drobné opotřebení počítače).

arron

Ono v drtivé většině případů se vyplácí uklízet menší nepořádek častěji než velký bordel jednou za čas. Člověk tím pak stráví dohromady řádově víc času a zašantročí víc věcí, které pak nemůže najít. Navíc žije celou tu dobu v bordelu. Fakt to za to stojí? :-)

Clary

A co když se jedná o koberce ve velkém hotelu, kterých je mnoho, neustále po nich někdo chodí, tahá zavazadla (souloží, zvrací, jí, močí, kálí atd.) a zákazníci hotelu při otm vyžadují aby koberce byly pořád perfektně čisté?

Honza

Jo, to je přesně to, proč si myslím, že to docela odpovídá. Pokud vám někdo zvrací na koberce každý týden, tak rozhodně potřebujete mít vlastní stroj na čištění, což přesně většina hotelů má (předpokládám). Já ho doma nepotřebuju.

Pokud má mít i tohle nějakou analogii k automatickým testům, tak pokud máte kód, který vám někdo mění každý týden a ještě při tom souloží, tak rozhodně potřebujete, aby to po něm někdo zkontroloval – a pokud základní kontrolu zvládne zadarmo automat, tak super.

jos

> Já ho doma nepotřebuju.

doma == pár commitů za měsíc

v hotelu == komerční projekt se stovkama+ commitů za měsíc

takže to vypadá že už se dostáváš k jádru věci a jak se k němu dostaneš, tak zjistíš že tohle srovnání s vysavačema je mimo; nakonec budeš rád, že sis napsal testy k nějakýmu domácímu projektu

a ještě k tomu čim si tohle vlákno začal – když si tak skeptickej a pořád hledáš co přesně unittesty neřeší … nechceš se na programování radši vysrat? dyť do takovýho stadia abys vůbec nedělal chyby nebo je po sobě našel klikáním po aplikaci před každým releasem nedolezeš – to vůbec nemyslim jako urážku, to je holej fakt

Honza

No, nemyslím si, že by srovnání bylo úplně dokonalé, ale když už v něm pokračuješ s velikostí projektu, tak nezáleží na tom, jak je barák velký, ale jak hodně se v něm špiní koberce. Čili pokud kód měním často, potřebuju testy, pokud jen leží, tak je psaní testů zbytečná námaha. Čili víceméně to, co říkáš ty, akorát mi přijde hloupé to vztahovat k velikosti projektu, protože že je projekt velký automaticky neznamená, že se na všech jeho částech nonstop aktivně pracuje.

Nehledám důvod, proč automatické testy nepoužívat, ale naopak důvod, proč je používat. Jsem skeptický, protože hledám způsob, jak si práci ulehčit (tzn. test mi pomůže) a ne způsob, jak si zbytečnou práci přidelat (tzn. budu psát test zbytečně).
S odpovědí, kterou mám zatím (tzn. pokud se kód měni, jsou testy potřeba a pokud ne, tak ne) jsem celkem spokojený, nicméně vnímám, že v reálu může být trochu problém dopředu odhadnout, která část kódu se bude měnit a která ne. Ale i to se dá praxí nacvičit.

Čili bych to viděl tak, že si počkám na další díly seriálu a uvidím :-)

arron

Další díly seriálu Ti budou IMHO k ničemu. Tohle totiž není o tom, v jakém frameworku se ty testy budou psát. Je to celé spíše filozofická otázka :-)

Ať už se ten projekt mění často nebo ne, před jeho odevzdáním stejně musí dojít k otestování. A unit testy (myslím si já) už v tu chvíli sehrají takovou roli, že se vyplatí :-) A i kdyby ne, a ten projekt se měnil jednou za rok, tak hned při první změně se to bohatě vrací zpět. Ono totiž při hledání chyb je ujištění o tom, že někde chyba není, poměrně klíčové :-)

Já vidím problém obecně v jiné věci. Myslím si, že ve firmách, které se zabývají online, se obecně netestuje. To, že někdo ten projekt tak jakože prokliká, tomu se nedá říct test. Natož, aby se dělalo regresní testování při každém uploadu čehokoliv na prokukční server. A samotný proces nahrávání už se vůbec netestuje (ačkoliv by měl). A v takovém prostředí není tolik vidět, jak široký dopad ty unit testy (a obecně automatizované testy) mají. Začít psát unit testy v takovém prostředí je, jako najmout bandu programátorů, která prochází kód pořád a pořád dokola a při každé změně provede komppletní sadu testů, takže je zajištěno i regresní testování (na úrovni unit testů samozřejmě). A to je obrovský přínos v kvalitě produktu (který nemusí byt vidět hned na první pohled).

Ve finále jsou unit testy jenom jední z nástrojů pro udržení kvality produktu v dlouhodobém měřítku, nicméně od nich se pak odvíjí další typy testů, šetří čas a nervy programátorů a tím umožňují firmě dlouhodobě růst a zvyšovat kvalitu svého výstupu.

Honza

Díky za komentáře, celkem asi chápu tvoje stanovisko, ale nijak zvlášť ho nesdílím. Myslím si ale, že rozhodně má smysl další díly seriálu číst, protože předpokládám, že až budu znát framework pro automatizované testování lépe, tak se budu moct lépe zamyslet nad tím, kdy testy používat a kdy ne.

Ještě jednu věc k tvému komentáři: „že někdo ten projekt tak jakože prokliká, tomu se nedá říct test“ – s tím souhlasím, ale stojím si za tím, že příklad testu v tomto článku ukázal, že „že někdo napíše automatický test, tomu se nedá říct testování“, protože i přesto, že automatický test prošel, tak chybu v kódu neobjevil.
Čili automatizované testy ano, ale nespoléhat pouze na ně a rozhodně nesdílím názor, „proklikávání stránek je k ničemu a automatické testy rovná se kvalitní kód“. Obě tyhle věci ti odhalí jiné chyby a na některé nepřijdeš ani tak ani tak.

arron

Tak dobře, ještě přecejenom odpovím ;-)

Když se podíváš o kousek výš (konec přízpěvku, který komentuješ), tak nemám pocit, že bych někde psal, že unit testy jsou samospasitelné a že zajistí 100% funkčnost. Ony „pouze“ zvyšují míru jistoty, že ten kód je v pořádku.

Na druhou stranu to, že unit testy automaticky zvyšují kvalitu kódu, za tím si dost stojím :-) Unit testy mě nutí lépe separovat kód, psát kratší a jednodužší třídy a metody (protože se daleko jednodušeji testují a já jsem líný), a nutí mě psát věci tam, kam opravdu patří (zase proto, že jinak se to o dost složitěji testuje).

Takže tak :-)

Honza

Ok, souhlasím.

arron

A je tu ještě jedno zajímavé hledisko. Spoustu lidí si řekne „začnem dělat ty unit testy.“ Po měsíci, když stráví opravdu nad všim dvojnásobek času s tím seknou, že je to k ničemu. Mám obavu, že relativné málo lidí si uvědomuje, že naučit se psát a používat unit testy je běh na relativně dlouho trať. Je potřeba se naučit používat úplně nové postupy, musí se změnit způsob přemýšlení programátorů, vyvíjí se různé nástroje k tomu, aby se unit testy psaly jednodušeji a podobně. Stejně jako s jakoukoliv jinou technologií, je to proces, který trvá řadu měsíců, spíše však jeden až dva roky.

A není to jenom o tom „začít psát unit testy“, ale i o tom, naučit se z toho správně profitovat a pochopit širší souvislosti :-)

Je to prostě i trochu filozofie ;-)

jos

> ale když už v něm pokračuješ s velikostí projektu

nic takovýho sem napřímo netvrdil, de spíš o cvrkot v tom projektu, v hotelu (i malým) mezi recepcí a pokojem leze daleko víc lidí se zablácenejma botama než u mě doma

jiná analogie:
řekněme že bydlíme vedle sebe a ty jezdíš do stejně vzdálený práce autobusem, je to levný, nemusíš lízt do garáže a votvírat vrata, zastávku máš přímo před barákem a před prací taky, super

já jezdim autem, musim se procpat garáží, lejt do nádrže drahý beňo, servisovat a vůbec se zatěžovat věcma kterejma se ty zabejvat v autobuse nemusíš

jenže po roce, kdy sem proti tobě střádal půlhodinky denně už je kurva znát že se jízda autem vyplatí

Honza

Jo, to je taky dobrá analogie, asi ještě lepší než s tím hotelem. Protože je na tom dobře vidět, že se to nemusí vyplatit vždy a všude. Tzn. pokud bydlím ve vsi za Prahou a pracuju na okraji Prahy, tak ušetřím spoustu času a budu to mít pohodlnější. Ale pokud bydlím na Florenci a pracuju na Václaváku, tak jezdit autem může jen magor.
Čili pro různé situace jsou vhodné různé postupy.

AntiHonza

Líbí se mi, že se od té potupy, jak ses představil pod prvním článkem, snažíš navodit dojem, jak se to snažíš chápat, ale prostě ti přijde pořád zbytečné. Je chvályhodné, že se takto vyvíjíš, ale proč to neustále odsuzování testů pod článkem v komentářích? Když nechceš testovat, tak netestuj a pak si případné problémy vysvětluj podle libosti.
Možná si jednou uvědomíš, že ty testy nejsou úplně zbytečné. Třeba až místo nějaké šablony pro WP budeš v týmu, který pracuje na rozsáhlejším projektu. Určitě není pravidlem, že by se hodily jen u velkých projektů. Mně se hodí testy pořád, když už máš více projektů a třeba se k jednomu vracíš po několika měsících, tak je snadné se pustit do úpravy kódu. Když nemám testy, tak to je jen takový výstřel, pak si sice otestuješ tu konkrétní změnu „proklikáním“, ale problém je v tom, že po delší době už občas něco zapomeneš otestovat atd. Nevím, jak přesně byl nějaký ten citát, ale s každou opravenou chybou zaneseš do kódu chyby další. Dodal bych, že bez testů jich zaneseš 5 až nekonečno, s testy 1 až 10. Čísla ber hodně s nadsázkou.
Možná už to padlo, pro unit testy existuje pěkná zkratka FIRST (http://pragprog.com/magazines/2012-01/unit-tests-are-first). S tvým stylem bych se zamyslel nad F a R. Nehodlám už na tebe nijak reagovat ani odpovídat, testuje ten, kdo chce. Když nechceš, tak je to tvoje věc, tak ale potom je přeci zbytečné komentovat každý článek, který tě v podstatě nezajímá a ani ti nic nepřináší.

Clary

Třeba si narozdíl od nás ostatních uvědomuje, že celé je to jen lobby testovacího průmyslu, který si koupil i takové lidi jako Robert C. Martin a Martin Fowler, aby o testování psali ve svých knihách :)

Honza

Díky za vzkaz, ačkoliv mi připadá poměrně agresivně laděný, takže se omlouvám, pokud jsem tě zbytečně naštval. Komentáře k prvnímu článku si budu muset asi znovu přečíst, potupu jsem nějak nevnímal.

Vcelku se nestydím za to, že neznám framework pro automatizované unit testy pro PHP a nepoužívám ho, ale nebráním se tomu se o něm něco dozvědět a třeba tento svůj postoj změnit, a proto čtu seriál. V komentářích se člověk dozví spoustu zajímavých doplňkových informací a názorů, takže celkem vidím přínos i v nich.
Ačkoliv ti přijde, že ve svých příspěvcích automatizované testy odsuzuju, tak se spíš snažím hledat nějaké argumenty nejen „pro“, ale i „proti“, hlavně proto, abych si ujasnil, kdy má a kdy nemá smysl automatizované testy používat.

A skutečnost, že automatický test neodhalí chybu, která v kódu v článku je, mi připadá jako poměrně zajímavá i pro jiné čtenáře, protože poukazuje na to, že automatizované testy nejsou samospásné a nemohou být jediným způsobem pro zajištění kvality výstupu. (Což možná někomu připadá samozřejmé, ale asi by to mělo být u takovéhleho seriálu explicitně řečeno.)

Jinak doufám, že ti to nebude vadit, ale budu číst seriál dál, protože mám pocit, že mi něco přináší, a případně to budu i komentovat.

Jakub Vrána

Představme si, že budeme chtít kód trochu zjednodušit. Třeba tím, že si výsledek $parser->getCode() uložíme do pomocné proměnné. Kód bude možná maličko přehlednější, nepatrně rychlejší, ale jinak bude všechno fungovat naprosto stejně. Tedy všechno, až na takto napsaný test.

Nebo si představme, že kód budeme chtít optimalizovat. Třeba zavoláním INSERT ... ON DUPLICATE KEY UPDATE místo SELECT; INSERT; UPDATE. Bude to zhruba dvakrát rychlejší, ale jinak to fungovat naprosto stejně. Tedy až na test.

Nebo si představme, že si seznam všech objektů načteme předem a uložíme je všechny do paměti, abychom je nemuseli načítat jednotlivě. Opět to bude zhruba dvakrát rychlejší, ale jinak to bude fungovat stejně. Až na test.

Takto napsaný test od testování odrazuje – jakákoliv změna v kódu znamená i změnu v testu. Údržba takovéhoto testu je skutečně jen zbytečná práce navíc a mnoho hodnoty nepřináší.

arron

Skoro si to říká o nějaký příklad, že? :-)

Jakub Vrána

„Aha“ efekt z tohoto příkladu by pro mě jako začátečníka byl ten, že psát testy vyžaduje neustálé synchronizování kódu a testu. Špatné příklady jsou nejhorší právě pro začátečníky, protože zkušenější pochopí, co tím chtěl asi autor říct.

Vhodný příklad by mohl testovat pomalou metodu, jejíž výsledky si kešuji. U takové metody bych chtěl ověřit, že se zavolá jen jednou, dokud nezměním cache key, kdy se zavolá znovu.

O implementaci skutečně nejde. Kód by mohl volat insertOrUpdate, což by někde fungovalo jako současné řešení a někde rychleji. Test by se opět rozbil.

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.