Komentáře k článku
Testování v PHP: odstiňujeme závislosti

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.
Testování pořadí volání metod
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
Re: Testování pořadí volání metod
IMHO PHPUnit nic takového nenabízí. Expektace je možné definovat jen každému mocku zvlášť.
Re: Testování pořadí volání metod
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
Tleskám
Konečně něco do praxe.
Re: Testování v PHP: odstiňujeme závislosti
Proč nepoužít rovnou integrační testování namísto složitých mocků?
Re: Testování v PHP: odstiňujeme závislosti
Právě proto, že chceme testovat jednotkově a ne integračně. Vrátím-li se k analogii s crash testy, je to stejné jako byste se ptal, proč nepoužívat lidskou osádku namísto zdlouhavě vyráběných dummies.
Re: Testování v PHP: odstiňujeme závislosti
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í.
Re: Testování v PHP: odstiňujeme závislosti
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?
Re: Testování v PHP: odstiňujeme závislosti
Bůh zabije koťátko ;-)
Re: Testování v PHP: odstiňujeme závislosti
To byla pouze analogie ;-) Jak píší kolegové níže, jde hlavně o přesnou identifikaci problému. Integrační testování se používá pro testování celku, zatímco my se zabýváme detailním testováním implementace.
Re: Testování v PHP: odstiňujeme závislosti
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)
Re: Testování v PHP: odstiňujeme závislosti
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.
Re: Testování v PHP: odstiňujeme závislosti
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í.
Použití mocku
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?
Re: Použití mocku
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“)
Re: Použití mocku
Da sa to riesit: set_new_overload(array($this, ‚_newCallback‘));
https://github.com/sebastianbergmann/php-test-helpers
Re: Použití mocku
Ano, to je jedna z hardcore možností. Bohužel ale řeší důsledek, ne příčinu. Podobné techniky je dobré používat jen pro legacy kód, který se chystáme refaktorovat.
Re: Použití mocku
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
Re: Použití mocku
Honzo, vydržte do druhé části seriálu, kde se těmto problémům budeme věnovat. Existují samozřejmě techniky, jak „podstrčit“ mock třídě, která si vytváří tvrdou závislost (např. instancializace v konstruktoru), ale samozřejmě platí – aby byla třída mockovatelná, tak musí být testovatelná. Pro tuto chvíli se podívejte na tyto tři zápisy:
Zatímco v prvním případě nebudete mít žádný problém vnutit třídě Db mock, ve druhém už to nebude vůbec jednoduché (budete nucen k nějakému „hackování“ třídy pomocí reflexe), ve třetím případě prakticky (běžnými postupy) nemožné.
Vždy byste se měl snažit aby se třída otevřeně hlásila ke svým závislostem a tyto příjímala namísto jejich in-line vytváření. Tento postup je znám jako Dependency injection.
Re: Použití mocku
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::getInstance()->begin();
$something = BazModel::getInstance()->getSomething();
BarModel::getInstance()->doSomething($something)
Transaction::getInstance()->commit();
}
}
Re: Použití mocku
Krasa tak to muzete rovnou zahodit a napsat znovu :-) .
Doporucuju zaroven prejit ze svn na git pokud ho jeste nepouzivate :-))
Re: Použití mocku
Toto je opravdu na odstřel :-)
Re: Použití mocku
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.
Re: Použití mocku
Díky za odpověď, myslel jsem si, že to tak nějak bude, ale vidět to takhle rozepsané rozhodně dost pomůže.
Mock na jednu metodu testovaného objektu
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
Re: Mock na jednu metodu testovaného objektu
Je to možné – do seznamu mockovaných metod zařadíš jen tu, kterou chceš překrýt (její chování mockuješ). Mrkni se na https://github.com/josefzamrzla/serial-testovani-v-php/tree/master/06-mocks/partialMock přidal jsem příklad tam. Testuji třídu Worker, která obsahuje metodu „work“, jejíž implementaci pro potřeby testu překryji. Metodu „checkResults“, která předchozí metodu volá, nezahrnu do seznamu mockovaných metod, takže bude volána její původní implementace.
Re: Mock na jednu metodu testovaného objektu
Díky moc. Vyzkouším.
Užitečný článek, zajímavá diskuse
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 :).
Mock vs. stub
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.