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

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

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

Články PHP, Různé

Jednou z velkých překážek unit testů jsou závislosti. Jak otestovat třídu, je-li závislá na jiných, které nechceme našimi testy ovlivnit? Přesně o tom bude dnešní díl o testování: jak odstranit, nebo lépe – nahradit, závislosti testovaných tříd.

V dnešním (a příštím) díle seriálu o testování uzavřeme pomyslnou část o unit testech a podíváme se na poslední velkou kapitolu, která nám ještě chybí a tou je odstiňování závislostí, také známé jako mockování. O co jde?

Izolace!

Bavíme-li se o unit testování, máme vždy na mysli testování tříd v úplné izolaci. V souvislosti s testováním často narazíte na pojem „System Under Test“, zkráceně SUT. V případě unit testů je SUT každá jednotka (třída) kódu. Ostatní jednotky NESMÍ být našim testem jakkoli ovlivněny! Jinak řečeno, třída, kterou testujeme, NESMÍ využívat žádný kód kromě svého vlastního, načítat externí data apod. Pokud toto porušíme, pak už se nejedná o unit test, nýbrž o integrační. A to rozhodně není naším cílem. Alespoň prozatím.

Jak ale testovat v izolaci jednotku, která má závislosti na jiných? Např. k její instancializaci jsou nutné instance jiných tříd? Odpověď je nasnadě – musíme je nahradit nějakými dvojníky, kteří budou potřebné závislosti zastupovat. Dvojníky v takových případech budou třídy požadovaného typu, implementující požadované rozhraní. Trochu moc obecný popis, ale vše bude brzo jasnější.

Jistou analogií této problematiky by mohly být crash-testy aut. Jistě se shodneme, že by nebylo úplně vhodné tyto testy provádět se skutečnou, lidskou, posádkou. Namísto toho se využívají zástupné postavy, které věrohodně zastupují skutečnou posádku. Mají podobné vlastnosti (výška, váha), nabízí podobné rozhraní (pohyb končetinami, pohyb hlavou).

Mock, stub a ti druzí

Nahrazování skutečných objektů zástupnými kopiemi, tzv. test doubles, se obecně říká „mockování“, ale ne vždy jde o správné označení. Kromě tzv. mocků totiž existují ještě další typy zástupných objektů a jak už to bývá, jejich názvy jsou často chybně vykládány nebo zaměňovány.

Dummy

Nejprostší ze všech dvojníků. Lze si jej představit jen jako placeholder. Používá se pouze ke splnění kontraktu testované třídy – např. parametr konstruktoru. Vrátíme-li se k analogii s crash-testy, pak jako dummy si můžeme představit např. atrapu motoru. Vypadá stejně, váží stejně, ale nemusí být funkční, protože ani nemáme v plánu jej pouštět. Je přítomen jen proto, že jej testované auto vyžaduje.

Fake

Tento dvojník už je malinko sofistikovanější, ale ne o moc. Implementuje požadované rozhraní, ale často velice prostě. Jeho úkolem je opět jen splnění kontraktu, tentokrát však s podmínkou, že může dojít k volání jeho metod. Proto musí být schopen tato volání obsloužit, byť nevrací žádné výsledky.

Stub

Pod pojmem stub už si můžeme představit poměrně zdařilého dvojníka. Na rozdíl dummy a fake jsou stubs ve většině případů generovány testovacím frameworkem. Na volání dokáží reagovat předpřipravenými výsledky nebo logikou a díky tomu docela věrně napodobují svou předlohu. Kromě toho často umí „sbírat“ data o tom, které jeho metody byly volány a kolikrát, s jakými parametry apod.

Mock

Od stubs už je jen malý krůček k mocks. Nejjednodušší definice říká, že mock je stub s expektacemi. Expektace si ukážeme už za malou chvíli, zatím alespoň v kostce – jde o sadu předem definovaných pravidel, kterými např. vyžadujeme určitý počet volání nějaké metody s přesnou sadou parametrů apod. Pokud po proběhnutí test case není některá z expektací splněna, celý test case je označen za neplatný (failed).

Vytváříme mock

Jak vidíte sami, rozdíl mezi stub a mock není moc velký. Možná i proto jsou tyto dva pojmy často zaměňovány a výjimkou bohužel není ani dokumentace PHPUnit. Jako stubs jsou zde označovány mocks, které mají nastaveny expektace na „libovolně-krát“ :-) I proto si vše trochu alibisticky ulehčíme a nadále se budeme věnovat už jen mockům. Ale dost už slovíčkaření, pojďme se podívat, co nám nabízí framework PHPUnit.

Úplně základní mock můžeme vytvořit dvěma způsoby. Oba si ukážeme na jednoduchém příkladu: máme třídu Db, jejíž konstruktor vyžaduje instanci typu Logger. Naším úkolem je otestovat třídu Db a to v úplné izolaci. O volání metod skutečného loggeru nemůže být řeč, musíme jej nahradit.

interface Logger
{
    public function log($message);
}

class Db
{
    /**
     * @var Logger
     */
    protected $logger;

    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }

    public function execute($query)
    {
        // ... kod metody "execute" ...

        $this->logger->log("Query: " . $query);
    }
}

Prvním, a často rychlejším, způsobem jak vytvořit mock, je pomocí metody getMock. Metoda má trochu strašidelný počet parametrů, ale zpravidla využijeme jen první tři.

  • $originalClassName – název typu, který chceme nahrazovat. Držme se raději označení „typ“ než „třída“, protože mock budeme často vytvářet proti rozhraní.
  • $methods – seznam (pole) metod, které budeme překrývat. Zde pozor! Metody, které v tomto seznamu neuvedeme, nebudou mockem překryty a bude volána jejich defaultní implementace! Pokud parametr úplně vynecháte, pak žádná rodičovská implementace volána nebude.
  • $arguments – pole parametrů vyžadované konstruktorem typu.
  • $mockClassName – tímto parametrem můžeme definovat vlastní název typu (třídy) vytvořeného mocku namísto automaticky vygenerovaného (např. Mock_Db_5f032524).
  • $callOriginalConstructor – boolean příznak určující, zda má být při instancializaci mocku zavolán konstruktor mockovaného typu. Defaultní hodnota je bool(true).
  • $callOriginalClone – boolean příznak určující, zda má při volání metody __clone dojít také k zavolání stejnojmenné metody mockovaného (rodičovského) typu. Defaultní hodnota je bool(true).
  • $callAutoload – boolean příznak, zda má být použit autoloading pro natažení mockovaného typu. Defaultní hodnota je bool(true).
  • $cloneArguments – boolean příznak určující, zda mají být parametry mockovaného objektu klonovány či nikoli. Defaultní hodnota je bool(false) a příliš nedoporučuji ji měnit, pokud si skutečně nejste jisti, co činíte. K tomuto parametru se ještě vrátíme příště, dokáže napáchat hodně nepříjemností.

Pokud vás tento počet parametrů děsí, pak můžete využít ještě tzn. mock builderu, což je v podstatě jen hezčí varianta výše uvedeného, v podobě fluent interface. Namísto parametrů tak zvolíte řetězení metod jakými jsou např. setMethods, setConstructorArgs, setMockClassName a další.

Zda zvolíte první či druhý způsob je čistě na vás. Můj tip – vytváříte-li mock, který kromě názvu mockovaného typu nepotřebuje žádné jiné nastavení, zvolte první způsob:

$mock = $this->getMock("stdClass");

Vytváříte-li mock, u kterého je nutné potlačit volání konstruktoru mockovaného typu, použijte druhý způsob:

$mock = $this->getMockBuilder("stdClass")
    ->disableOriginalConstructor()->getMock();

Je to přehlednější než řada defaultních parametrů. Pojďme si pro ukázku napsat mock požadovaný našim testem a projít výsledný kód řádek po řádku:

class DbTest extends PHPUnit_Framework_TestCase
{
    public function testExecute()
    {
        $loggerMock = $this->getMock("Logger");
        $loggerMock->expects($this->once())
            ->method("log")
            ->with($this->equalTo("Query: foofoo"));

        $db = new Db($loggerMock);
        $db->execute("foofoo");
    }

}

Metoda pro vytvoření mocku už by nám měla být známá. Protože chceme vytvořit jednoduchý mock, ignorujeme všechny parametry kromě prvního – název mockovaného typu, kterým je v našem případě rozhraní Logger.

$loggerMock = $this->getMock("Logger");

Nyní jsme vytvořili v podstatě fake. Je typu Logger, ale nic konkrétního neumí. Na jakékoli volání bude odpovídat hodnotou null. Mock z něj udělají až zmíněné expektace. Jednu mu tedy přidáme, očekáváme, že v testu bude právě jednou volána jeho metoda log:

$loggerMock->expects($this->once())
        ->method("log");

Pokud nechceme definovat žádná další omezení, pak máme hotov už opravdový mock, který nám ohlídá, že v testu došlo k právě jednomu zavolání metody jménem log. Nestane-li se tak, test case selže. My ale přidáme ještě jedno omezení a tím řekneme, že očekávané volání metody log má být s jedním parametrem – řetězcem „Query: foofoo“. V testu se chystáme volat testovanou metodu execute s parametrem „foofoo“, proto právě tento řetězec.

$loggerMock->expects($this->once())
        ->method("log")
        ->with($this->equalTo("Query: foofoo"));

Expektace

Jak už jsem uváděl výše, pod pojmem expektace si můžeme představit určitou sadu pravidel, která musí být v průběhu test case splněna, jinak dojde k selhání testu. V našem prvním příkladu jsme nastavili hned dvě:

  1. bude právě jednou zavolána metoda jménem log
  2. volání metody log bude s uvedeným parametrem („Query: foofoo“)

Základní a nejpoužívanější expektací je předpokládaný (a vyžadovaný) počet volání metody mocku. Její nastavení provedeme ve dvou krocích, jako první musíme nastavit tzv. invocation matcher. Nenapadá mě vhodný český ekvivalent, proto raději zůstaneme u tohoto označení. K nastavení invocation matcher PHPUnit nabízí dvě metody:

  • expects – očekáváme volání instanční metody
  • staticExpects – očekáváme volání statické metody

Obě metody přijímají jeden parametr – instanci třídy implementují rozhraní PHPUnit_Framewor­k_MockObject_Matcher_Invo­cation. Ale není třeba se děsit, framework má několik před-definovaných matcherů, které je možné získat pomocí shortcut methods instance PHPUnit_Framework_TestCase. Pojďme se na ně podívat:

Třída Zkratka Popis
*_AnyInvokedCount $this->any() Vrací matcher, který ověřuje, zda uvedená metoda (budeme ji definovat později) byla zavolána libovolněkrát.
*_InvokedCount $this->never() Vrací matcher, který ověřuje, zda uvedená metoda nebyla nikdy zavolána.
*_InvokedAtLeastOnce $this->atLeastOnce() Vrací matcher, který ověřuje, zda uvedená metoda byla zavolána alespoň jednou.
*_InvokedCount $this->once() Vrací matcher, který ověřuje, zda uvedená metoda byla zavolána právě jednou.
*_InvokedCount $this->exactly(int $count) Vrací matcher, který ověřuje, zda uvedená metoda byla zavolána přesně tolikrát, kolik zadáme pomocí parametru $count.
*_InvokedAtIndex $this->at(int $index) Vrací matcher, který ověřuje zda uvedená metoda byla zavolána v pořadí, definovaném pomocí parametru $index (počítáno od nuly). Říká se mu také sekvenční matcher, používá se pro kontrolu pořadí volání.
(*) PHPUnit_Framework_MockObject_Matcher

Druhým krokem definice základní expektace je předání názvu metody, jejíž volání očekáváme. Toto provedeme jednoduše pomocí metody method(), viz. první příklad. Sice by to mělo být zřejmé, ale raději upozorním na fakt, že není možné mockovat metody, které jsou staticfinal nebo private!

Pokud nám nezáleží na tom, s jakými parametry bude metoda volána ani od volání metody nevyžadujeme žádnou návratovou hodnotu, pak máme nejjednodušší mock hotov. Ne vždy je to ale našim cílem, proto pojďme náš mock obohatit ještě o expektaci parametrů.

Přesně k tomu slouží metoda with() s proměnným počtem parametrů. Jednotlivými parametry jsou pak buď přímo hodnoty, které při volání očekáváme nebo instance constraints. Pokud metodě with() předáte jako parametr cokoli jiného než instanci constraint, pak framework defaultně parametr převede na constraint  PHPUnit_Frame­work_Constraint_IsEqual. Následující zápisy jsou tak ekvivalentní:

with(1, "foobar");
with($this->equalTo(1), $this->equalTo("foobar"));
with(
    new PHPUnit_Framework_Constraint_IsEqual(1),
    new PHPUnit_Framework_Constraint_IsEqual("foobar"));

Doplňujeme funkčnost

Tím jsme se dostali zpět k našemu příkladu a už bychom měli být schopni psát jednoduché mocky s expektacemi na počet volání a parametry metod. Pozorní čtenáři ale budou asi trochu protestovat, protože podle terminologie uvedené na začátku tohoto dílu, jsme zatím nevytvářeli mock, ale spíše něco jako „fake s expektacemi“. Správný mock by přece měl předstírat funkčnost své předlohy. Našemu mocku chybí přesně ta část, které se říká stub.

Pod pojmem stub je v PHPUnit myšlena třída implementující rozhraní PHPUnit_Framewor­k_MockObject_Stub a framework nám opět nabízí několik prefabrikovaných. Stejně jako v případě invocation matchers, i zde můžeme využít zkratky v podobě metod instance PHPUnit_Framework_TestCase:

Třída Zkratka Popis
*_Return $this->returnValue($value); Při volání metody vrátí zadanou hodnotu. Hodnotou může být cokoli, framework ji nijak nemodifikuje.
*_ReturnArgument $this->returnArgument($argumentIndex); Při volání metody vrátí parametr určený zadaným indexem (počítáno od nuly). Seznam parametrů je určen metodou with().
*_ReturnCallback $this->returnCallback($callback); Při volání metody zavolá zadaný callback s parametry, definovanými v metodě with().
*_ReturnSelf $this->returnSelf(); Při volání metody vrátí sám sebe (instanci mocku).
*_ReturnValueMap $this->returnValueMap(array $valueMap); Při volání metody vrací předem definovanou mapu hodnot, což jsou pole nesoucí jak vstupní parametry, tak návratovou hodnotu.
*_ConsecutiveCalls $this->onConsecutiveCalls([args]); Při opakovaných voláních metody jsou postupně (sekvenčně) vraceny zadané hodnoty. Pokud je vracená hodnota instancí implementující PHPUnit_Framework_MockObject_Stub, pak je nejprve zavolána její metoda invoke.
*_Exception $this->throwException(Exception $exception); Při volání metody vyvolá zadanou výjimku.
(*) PHPUnit_Framework_MockObject_Stub

V přehledu se objevila celá řada neznámých pojmů, pojďme si vše ukázat na příkladech.

returnValue

Tento stub jsme už použili v prvním příkladu, jeho chování je přímočaré – jednoduše vrátí zadanou hodnotu.

returnArgument

Stub podobný předchozímu, jen při volání mockované metody, namísto konkrétní hodnoty, vrátí některý z jejích parametrů, určený pořadím (počítáno od nuly).

class ReturnArgumentTest extends PHPUnit_Framework_TestCase
{
    public function testReturnArgument()
    {
        $mock = $this->getMock("stdClass", array("foo"));
        $mock->expects($this->once())
            ->method("foo")
            ->with("bar", "baz")
            ->will($this->returnArgument(0));

        $this->assertEquals("bar", $mock->foo("bar", "baz"));
    }
}

returnCallback

Pokud potřebujeme pomocí stub simulovat nějakou logiku, např. výpočet, můžeme s úspěchem použít tento stub. Jako callback lze předat cokoli, co splňuje type hint callable (viz.http://www.php.net/ma­nual/en/language.types.ca­llable.php).

class ReturnCallbackTest extends PHPUnit_Framework_TestCase
{
    public function testReturnCallback()
    {
        $mock = $this->getMock("stdClass", array("calc"));
        $mock->expects($this->exactly(3))
            ->method("calc")
            ->will($this->returnCallback(
                function($x, $y) {
                    return $x + $y;
                }
            )
        );

        $this->assertSame(3, $mock->calc(1, 2));
        $this->assertSame(0, $mock->calc(-2, 2));
        $this->assertSame(0, $mock->calc(0, 0));
    }

    public function testReturnForeignCallback()
    {
        $mock = $this->getMock("stdClass", array("calc"));
        $mock->expects($this->exactly(3))
            ->method("calc")
            ->will($this->returnCallback(array($this, "calcCallback")));

        $this->assertSame(1, $mock->calc(0, 1));
        $this->assertSame(2, $mock->calc(-2, 4));
        $this->assertSame(3, $mock->calc(3, 0));
    }

    public function calcCallback($x, $y)
    {
        return $x + $y;
    }
}

returnSelf

Jak bylo uvedeno v přehledu – tento stub vrací instanci mocku. K čemu je to dobré? Např. pro testování fluent interface.

class ReturnSelfTest extends PHPUnit_Framework_TestCase
{
    public function testReturnSelf()
    {
        $mock = $this->getMock("stdClass", array("foo", "bar"));
        $mock->expects($this->exactly(2))
            ->method("foo")
            ->will($this->returnSelf());

        $mock->expects($this->once())
            ->method("bar")
            ->will($this->returnSelf());

        $this->assertSame($mock, $mock->foo());
        $this->assertSame($mock, $mock->foo()->bar());
    }
}

returnValueMap

V přehledu bylo předesláno, že stub vrací cosi jako mapu hodnot, která obsahuje jak vstupní parametry, tak návratovou hodnotu. Jakkoli to zní krkolomně, skrývá se za tím prostá myšlenka. Pokud si vzpomínáte, tak v minulém díle, věnovanému anotacím, jsme si ukazovali anotaci jménem @dataProvider. Toto je v podstatě její obdoba. Předem si definujeme pole, jehož posledním prvkem bude návratová hodnota metody a všechny prvky před ním budou metodě předány jako parametry. Ukázka vše objasní:

$map = array(
    array(1, 2, 3),
    array(2, 2, 4),
    array(5, 5, 10)
);

Zavoláme-li mockovanou metodu s parametry: 1 a 2, pak návratová hodnota bude: 3. Zavoláme-li ji s parametry: 2 a 2, návratová hodnota bude: 4, a tak dále… Co se stane v případě, že budeme volat metodu s kombinací parametrů, která v mapě není? Jednoduše vrátí null.

class ReturnValueMapTest extends PHPUnit_Framework_TestCase
{
    public function testReturnValueMap()
    {
        $map = array(
            array(1, 2, 3),
            array(2, 2, 4),
            array(5, 5, 10));

        $mock = $this->getMock("stdClass", array("calc"));
        $mock->expects($this->any())
            ->method("calc")
            ->will($this->returnValueMap($map));

        $this->assertSame(10, $mock->calc(5, 5));
        $this->assertSame(4, $mock->calc(2, 2));


        $this->assertNull($mock->calc(5, 6));
    }
}

onConsecutiveCalls

Tento stub je jednodušší variantou předchozího. Volání mockované metody postupně vrací zadané hodnoty. Opět se nabízí otázka – co bude vráceno, pokud počet volání překročí definovaný počet návratových hodnot? Stejně jako v předchozím případě – je vráceno null.

class OnConsecutiveCallsTest extends PHPUnit_Framework_TestCase
{
    public function testOnConsecutiveCalls()
    {
        $mock = $this->getMock("stdClass", array("getValue"));
        $mock->expects($this->exactly(4))
            ->method("getValue")
            ->will($this->onConsecutiveCalls(5, 3, 1));

        $this->assertSame(5, $mock->getValue());
        $this->assertSame(3, $mock->getValue());
        $this->assertSame(1, $mock->getValue());
        $this->assertSame(null, $mock->getValue());
    }
}

throwException

Poslední z popisovaných stubs, má jednoduchý úkol – při volání mockované metody vyvolat výjimku. V ukázce použijeme anotaci známou z minulého dílu – @expectedException.

class ThrowExceptionTest extends PHPUnit_Framework_TestCase
{
    /**
     * @expectedException InvalidArgumentException
     */
    public function testThrowException()
    {
        $exception = new InvalidArgumentException();

        $mock = $this->getMock("stdClass", array("foo"));
        $mock->expects($this->once())
            ->method("foo")
            ->will($this->throwException($exception));

        $mock->foo();
    }
}

Sekvenční matcher „at“

Téměř všechny stuby uvedené v ukázkách měly za úkol řízení návratových hodnot mockovaných metod. Občas se ale můžete setkat s následujícím problémem – nezáleží úplně na návratové hodnotě, ale je nutné aby byla mockovaná metoda poprvé zavolána s jednou, přesnou, sadou parametrů, podruhé s jinou, opět přesnou, sadou parametrů. Dost možná vás napadne řešení podobné tomuto:

class MatcherAtTest extends PHPUnit_Framework_TestCase
{
    public function testMatcherAt()
    {
        $mock = $this->getMock("stdClass", array("foo"));
        $mock->expects($this->once())
            ->method("foo")
            ->with(1, 2, 3);

        $mock->expects($this->once())
            ->method("foo")
            ->with(4, 5, 6);

        $mock->foo(1, 2, 3);
        $mock->foo(4, 5, 6);
    }
}

Poprvé nastavíme právě jedno volání metody s první sadou parametrů, podruhé opět právě jedno volání s druhou sadou parametrů. Tento zápis ale bohužel fungovat nebude, protože druhým nastavením si pouze přepíšete to první a test nám selže s hlášením, že namísto očekávané hodnoty prvního parametru „4“ byla metoda volána s prvním parametrem rovným „1“.

Správným řešením je použít sekvenční matcher at(), kterým můžeme definovat přesnou posloupnost volání mockované metody.

class MatcherAtTest extends PHPUnit_Framework_TestCase
{
    public function testMatcherAt()
    {
        $mock = $this->getMock("stdClass", array("foo"));
        $mock->expects($this->at(0))
            ->method("foo")
            ->with(1, 2, 3);

        $mock->expects($this->at(1))
            ->method("foo")
            ->with(4, 5, 6);

        $mock->foo(1, 2, 3);
        $mock->foo(4, 5, 6);
    }
}

Jako vždy – všechny ukázky, jak z tohoto, tak i z předchozích dílů seriálu najdete na mém Githubu:

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

Příště

Příště se podíváme na zbytek problematiky odstiňování závislostí. Krátce probereme mockování webservices a file systému a vše vyzkoušíme na praktickém příkladu.

Komentáře

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

Super článek :-)

Celé je to fakt hezky vymakané, nicméně pokud chci udělat opravdu dobrý unit test, tak musím opravdu testovat pořadí volání jednotlivých závislostí. Tak jak je to popisováno v článku, tak se dá, docela pohodlně, testovat pořadí volání v rámci jedné závislosti. Jak to ale řešit, když se bude volat víc závislostí a budu testovat i pořadí volání mezi nimi (tzn. nejdřív se zavolá jedna závislost, pak druhá a pak zase ta první a tohle pořadí potřebuji otestovat)? Je možné tohle s pomocí phpunitu vyřešit? Nebo je na to potřeba si podporu přidat?

Díky moc

arron

To je přesně, co jsem jsi myslel :-) říkal jsem si jenom, jestli jsem náhodou nějakou techniku nepřehlédnul :-)
Díky moc

michal

Konečně něco do praxe.

náhodný čtenář

Proč nepoužít rovnou integrační testování namísto složitých mocků?

Martin Hassman

Já bych ještě argumentoval filosofickým rozdílem:
* Unit testy – ověřuji, zda jsem kód napsat správně (zda dělá, co má, zadané vstupy funkcí vrací očekávané výstupy).
* Integrační testy – ověřuji, zda ty správně napsané kusy kódu spolu správně komunikují.

náhodný čtenář

Promiňte, ale to je zcela uhozený příklad. Lidskou osádku nepoužíváme proto, že by ti lidé utrpěli zranění, případně zemřeli. Co zemře, když budu třeba načítat skutečná data z databáze namísto mockovaných dat?

Clary

Bůh zabije koťátko ;-)

Clary

Cílem je identifikovat místo vzniku chyby. V integračním testu, kde je mnoho závislostí, se velmi špatně hledá na které konkrétní věci test spadnul.

Také pokud jsem napsal třídu, která je jedntokově testovatelná (tj. má vyřešené závsislosti a splňuje některé další atributy) můžu si být zase o něco jist, že je tato třída napsána dobře (v rámci čistého objektového návrhu)

Mystik_7

Integrační test pomůže odhalit, že někde v systému je chyba. Jednotkový test pomůže odhalit, kde ta chyba je.

Další rozdíl je, že jednotkové testy jsou rychlé a je je tedy možné spouštět často – mnohdy se doporučuje je spouštět po každém commitu.

Integrační testy jsou obvykle řádově pomalejší a proto se spouštějí méně často. Například jednou za hodinu.

Cílem jednotkových testů je pak co nejrychleji odhalit možný problém. Cílem integračních testů je pokrýt oblasti, které jednotkové testy vynechaly a ujistit se, že systém funguje jako celek.

arron

Integrační testy samozřejmě použít můžete, otázka je, jestli z nich budete mít takový užitek :-)

Integrační testy jsou výborná věc, otestují celý systém, jestli spolu jednotlivé komponenty dobře spolupracují a podobně. V to je jejich velká síla. A zároveň slabina…pokud nastane v integračních testech chyba, nemusí být na první pohled jasné, co přesne chybu způsobilo. Je potřeba se hrabat v různých log souborech a s trochou smůly začít eliminovat jednotlivé nápady, kde mohla chyba vzniknout…což je ve většině případů pomalé až velmi pomalé…

Trochu jiná nastává situace, kdy můžeme jednotlivým komponentám „věřit“, že opravdu dělají to, co bylo zamýšleno, aby dělaly. Tím se zásadně zužuje seznam možností selhání.

A přesně v tuhle chvíli nastupují unit testy. Jsou (mají být!) velmi rychlé a protože testují jenom velmi omezenou část kódu, takže příčina chyby se daleko lépe lokalizuje. Navíc jí programátor zachytí velmi rychle, protože spouští unit testy před každým commitem, popřípadě je CI prostředí spoučtí automaticky a chyba se tak objeví opravdu v řádu minut. Mimojiné i proto, že programátor mám problém ještě v hlavě, dokáže zpravidla opravdu rychle odhalit jeho příčinu.

Tzn. do integračních testů by měl vstupovat o unittestovaný kód o kterém můžeme tvrdit, že každá jeho část dělá to, co bylo zamýšleno a dále testujeme opravdu už jenom integraci jako takovou. Jde to samozřejmě dělat včechno dohromady v integračních testech a ačkoliv si dovedu představit prostředí, kde by se to vyplatilo, obecně se to považuje za pomalé a méně efektivní.

Honza

Asi jsem něco přehlédl, ale nenašel jsem v článku, jak donutím testovaný objekt použít mock místo skutečné instance mockované třídy, pokud si ji například vytváří v konstruktoru nebo se k ní dostane nějak jinak?

arron

Nic jste nepřehlédl. Ono to totiž nejde.

Pokud je v konstruktoru něco jako new SomeClass(); tak se s tím nedá nic dělat. Je to chyba návrhu a je potřeba návrh upravit, aby třída byla testovatelná. A to je přesně ten bod, kdy vám unit testy pomohou zlepšít návrh a de facto vás „donutí“ začít používat některé „správné praktiky“ (zde například DI) :-)

Pokud vytváříte objekt přes nějakou factory, tak potom záleží, jestli tuto factory dostává objekt zvenku. Pokud ano, uděláte mock factory a je v podstatě hotovo :-) (pod tím si představuju to „nějak jinak“)

5o

Da sa to riesit: set_new_overlo­ad(array($this, ‚_newCallback‘));

https://github.com/sebastianbergmann/php-test-helpers

arron

Heleme se, o tom jsem nemél ani tušení :-) V tom php se fakt dá naprasit fakt uplně cokoliv, jak tak na to koukám :-D

Clary

U nás ve firmě se ještě s oblibou používají globální „singletony“ :-/ (jenom se tváří jako singleton, konstruktory jsou veřejné) a to asi tímto způsobem:

class BazModel
{
function foo()
{
Transaction::ge­tInstance()->begin();
$something = BazModel::getIn­stance()->getSomething();
BarModel::getIn­stance()->doSomething($so­mething)
Transaction::ge­tInstance()->commit();
}
}

Michal

Krasa tak to muzete rovnou zahodit a napsat znovu :-) .
Doporucuju zaroven prejit ze svn na git pokud ho jeste nepouzivate :-))

arron

Tohle je sice opravdu šílený, ale zase se to docela jednoduše (a poměrně bezpečně) dá rychle refaktorovat. Ono jenom zavření získávání instance do metod ten kód dost zlepší a přitom se to dá udělat skoro automaticky. A od toho už je jenom krůček k tomu, aby se tam ty instance odněkud injectovali.

Takže rozhodně nemazat, ale postupně refaktorovat :-) Veřte, že výsledek bude lepší než to psát celé znovu.

Honza

Díky za odpověď, myslel jsem si, že to tak nějak bude, ale vidět to takhle rozepsané rozhodně dost pomůže.

Jirka

Měl bych dotaz. Mějmě třídu, která má dvě metody. Řekněme metodu A a metodu B. Metoda A provádí nějaký výpočet a jedním ze vstupů tohoto výpočtu je výsledek metody B.

Př.

public function A() {
return 2*$this->B();
}

A teď si představme, že chceme otestovat funkčnost metody A, ale zároveň do toho testu nezahrnout testování funkčnosti metody B. V podstatě něco jako udělat Mock jen na některých metodách testovaného objektu. Prostě tak aby A fungovala normálně, zatímco B vrácela „podvržené výsledky“.

Lze tohoto chování v PHPUnit nějakým způsobem docílit, nebo to prostě nelze a nebo je to uhozená myšlenka jdoucí proti nějakému pravidlu ať už OOP nebo jednotkového testování?

Jedno z možných řešení, na které jsem narazil je vytvořit odvozenou třídu jen pro testování. Tato testovací třída by metodu A podědila a metodu B přeimplementovala podle požadavku testu. Toto řešení mi ale příjde neelegantní, něco jako hrabat se levou rukou za pravým uchem (příp. pro praváky obráceně).

Moc děkuji za nakopnutí příp. za vysvětlení proč to nelze. Jirka

Jirka

Díky moc. Vyzkouším.

Podbi

Další kvalitní článek o testování a k tomu zatím i dost zajímavá a podnětná diskuse. Jen tak dál! Těším se na další díl :).

Jan Prachař

Mohl jste zmínit, že pro test doubles používáte názvosloví dle Martina Fowlera, nicméně není příliš ustálené a přijde mi, že každý testovací framework si to pojmenovává po svém.

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.