Mock testing - Potěmkinovy vesnice
Řada z vás možná už na výraz Mock testing narazila, někteří ne. Pro ty z vás, kteří Mock přístup v testování nepoužili je tento článek. Pro ostatní může být zajímavá ukázka této techniky na knihovně EasyMock.
- Co jsou to mock objekty?
Jedná se vlastně o techniku psaní určitého druhu automatických testů. V podstatě se jedná o nahrazení reálného objektu testovací fasádou, která neprovádí žádnou funkcionalitu nahrazovaného objektu - jen se jako tento objekt tváří. Místo původní logiky objektu je vloženo chování, které ve svém testu potřebujete.
Nejlepší bude teorii provázat s praxí. Pokud píšete automatické testy, jistě jste narazili na situaci, kdy k napsání jednoduchého testu musíte velmi složitě a pracně připravovat okolní podmínky. Např. testujete metodu v business objektu, který se dotazuje interně DAO objektů na data v databázi. To ale znamená, že před tím, než začnete testovat logiku tohoto business objektu, musíte připravit správná data v databázi. Musíte také zajistit, že se tam omylem nedostanou další data, která by test zhatila (např. v SELECT dotazu vrátila více řádků) a tudíž obvykle zase musíte po sobě uklízet. Ošetření okolních podmínek spuštění testu vás nakonec může stát několkrát více času, než potřebujete k napsaní vlastní testovací logiky.
Existují různé techniky, jak toto zajistit - setkal jsem se např. s použitím HSQL databáze, která byla celá v paměti a po každém testu se kompletně dropla a před novým opět vytvořila a dosadila se příslušná testovací data (tím, že se vše odehrávalo pouze v paměti byly testy velmi rychlé). Ovšem lehčí je dle mého názoru právě použít techniku mock objektů.
Při použití této techniky se soustředíte na testování pouze logiky business objektu a předpokládáte že okolní objekty fungují správně tak jak mají. Potom v našem příkladě za DAO objekty dosadíte pouze “prázdné” ulity, které mají stejné rozhraní, ale místo vlastní logiky obsahují logiku takovou, kterou potřebujete pro vlastní test. Tzn. ve chvíli, kdy se business objekt zeptá DAO na konkrétní data, neproběhne dotaz do databáze, ale vy mu rovnou vrátíte data, která v daném testu potřebujete.
To je celý princip mock objektů. Nic víc, nic míň. Pokud se chcete dozvědět něco víc, pokračujte ve čtení.
- Stub objekty a Mock objekty
Pokud chcete použít tuto techniku při testování, potřebujete vyřešit dva problémy:
- možnost nastavit testovanému objektu mock objekt zvenčí - princip IoC
V podstatě to znamená, abyste mohli nahradit původní reálný objekt náhražkou např. přes setter metodu, konstruktor nebo jako parametr testované metody. Velmi prosté, ale v některých případech, kdy nemáte pod kontrolou API testovaného objektu, to vylučuje samotné použití této techniky. - možnost nahradit originální logiku nahrazovaného objektů testovací logikou - princip OOP
Tzn. testovaný objekt nesmí poznat, že pracuje s náhražkou. Musí být zachováno rozhraní původního objektu a objekty musí být typově stejné.
To se dá v Javě udělat jen dvěma způsoby: děděním nebo implementací interface. Vytvářet mocky děděním bych nedoporučoval, jelikož potom máte v mock objektu fragmenty původní logiky, což vám to může mnohokrát akorát celé zamotat. Ideální je tedy nadefinovat rozhraní, se kterým testovaný objekt pracuje a dané rozhraní je implementováno dvěmi třídami - tou reálnou a falešnou “mock” implementací. Je to trochu pracnější způsob, ale čistší.
Ve vlastním testu potom jednoduše vytvoříte testovaný objekt, nastavíte mu místo původní reálné implementace testovací náhražku a zajistíte, aby ta vracela data, která ve svém testu požadujete.
Můžete se vydat cestou tzv. stub objektů - což znamená že vytvoříte fyzicky třídy, které obsahují logiku, kterou od nich požadujete v testech. Je to asi jednodušší cesta pro někoho, kdo s tímto způsobem testů ještě nepřišel do styku, ale cesta dosti pracná.
Dalším vývojovým stupněm jsou tzv. mock objekty. Jejich použití je možná obtížnější, zato přinášejí jednu zásadní výhodu. Dynamické mock objekty jsou vytvářeny přímo testem a existují pouze virtuálně. Tzn. není třeba fyzicky implementovat vlastní mock třídu a navíc vám zdarma poskytnou řadu funkcionality, kterou byste do svých stub objektů pracně implementovali. Příklad použití mock objektů si ukážeme na použití knihovny EasyMock.
- EasyMock knihovna a vytvoření jednoduchého testu s použitím mock objektu
Dejme tomu, že máme tento business objekt, který chceme testovat.
Ten používá ke své činnosti dvě rozhraní - IMessageSender a ILogger. Vlastní logika business objektu je velmi prostá, pokud dojde číslo menší jak 10, pošle email příjemci jedna. Pokud je číslo větší, pošle mail příjemci dva. Pokud se nepodaří odeslat email, tuto chybu zaloguje. Uvedená dvě rozhraní jsou zobrazena níže.
Výše uvedenou logiku bychom normálně obtížně testovali, ale s použitím mock objektů to bude triviálně jednoduché. Kompletní zdrojové kódy jako Maven 2 projekt naleznete na konci článku. V něm je implementace testů jak pomocí Mock, tak i Stub objektů. Nuže, jak vypadá testování s knihovnou EasyMock?
Test první:
V prvním testu chceme otestovat, že pokud je vstupní hodnota 1 odešle se email prvnímu příjemci a zároveň se nezaloguje žádná chyba (odeslání se podaří).
Ve výše uvedeném kódu je několik zajímavých míst.
#1 - zde se vytváří virtuální mock objekt implementující specifikované rozhraní; fakticky zde v tuto chvíli knihovna EasyMock vytvoří novou třídu implementující vaše rozhraní a instanciuje objekt této virtuální třídy;
#2 - zde zavoláme metodu našeho rozhraní stejně, jako bychom to udělali s reálným objektem; v tomto případě, však pouze instruujeme mock objekt, že má očekávat (po odstartování skutečného testu - viz. bod #3) volání metody send s uvedenými parametry
#3 - zde řekneme knihovně EasyMock: až do tohoto bodu jsme programovali tvé chování pro daný test; od této chvíle se chovej dle našich instrukcí (viz. bod #2)
#4 - nastavíme mock objekty do testovaného objektu - tzn. místo původních reálných implementací mu podstrčíme kukaččí vejce v podobě našich mock objektů
#5 - zavoláme testovanou metodu business objektu - ta proběhne standardním způsobem a business objekt při svém chodu zavolá metodu rozhraní IMessageSender na našem mock objektu - volání proběhne bez problémů, tudíž si business objekt myslí, že vše proběhlo v pořádku (náš mock objekt se tvářil jako reálný objekt)
#6 - verifikace mock objektů - toto je hlavní pasáž mock testování; zde EasyMock prověří, zda celá akce proběhla tak, jak jste ho před replay fází naučili; tzn. ověří, že mezi replay a verify došlo k jednomu volání metody send na messageMock objektu a že vstupní parametry odpovídaly (equals) vámi definovaným hodnotám z bodu #2; pokud by k volání nedošlo, nebo došlo vícekrát, či některá z hodnot se lišila od vámi specifikovaných, dojde zde k vyjímce a celý test selže; taktéž si povšimněte že součástí verify je i loggerMock objekt, u kterého jsme neprováděli žádnou “instruktáž” - v takovém případě ve fázi verify ověří EasyMock knihovna, že nebyla zavolána žádná metoda tohoto rozhraní (což je dobře, protože v takovém případě by se business objekt zachoval špatně)
Test druhý:
V druhém testu chceme simulovat chybu odeslání zprávy. Objekt implementující IMessageSender rozhraní by v metodě send měl vyvolat chybu SendException. Business objekt by na to měl reagovat zalogováním chyby rozhraním ILogger.
#1 - vytvoříme business objekt
#2 - opět vytvoříme virtuální mock objekty
#3 - nyní instruujeme EasyMock, že má očekávat volání metody logError na objektu loggerMock - přičemž první parametr musí být ne null objekt (String) a druhým parametrem má být objekt přetypovatelný na třídu SendException (v této části vidíte, že argumenty nemusíte uvádět exaktně přesně - EasyMock poskytuje abstraktnější výrazové prostředky)
#4 - zde instruujeme EasyMock, že má očekávat volání metody send na objektu messageMock s konkrétními vstupními hodnotami (obdobně jako v testu 1)
#5 - zde je další novinka, zde říkáme EasyMocku aby v posledním volání (viz. bod #4) vyhodil jako výsledek volání exception SendException (obdobně bychom mohli EasyMocku říct, aby poslední volání vrátilo konkrétní návratovou hodnotu záměnou andThrow voláním metody andReturn)
#6 - nyní řekneme knihovně EasyMock: až do tohoto bodu jsme programovali tvé chování pro daný test; od této chvíle se chovej dle našich instrukcí
#7 - nastavíme mock objekty do testovaného objektu - tzn. místo původních reálných implementací mu podstrčíme kukaččí vejce v podobě našich mock objektů
#8 - zavoláme testovanou metodu business objektu - ta proběhne standardním způsobem a business objekt při svém chodu zavolá metodu rozhraní IMessageSender na našem mock objektu - při volání metody send náš mock vyhodí SendException a náš business objekt by se měl patřičně zareagovat zalogováním chyby
#9 - verifikace mock objektů - EasyMock ověří, zda na messageMock objektu byla zavolána právě jednou metoda send a parametry volání odpovídaly (equals) předpisu z kroku #4; dále ověří, že na objektu loggerMock byla právě jednou zavolána metoda logError s prvním ne nullovým parametrem a druhým parametrem přetypovatelným na SendException
- Další možnosti knihovny EasyMock
Výše uvedené příklady prezentují pouze základní a jednoduché možnosti knihovny EasyMock. V druhé verzi má již poměrně rozsáhlé možnosti “imitace” a ověřování požadovaného chování. Například umožňuje parametry porovnávat ne jen na ekvalitu, ale má řadu výrazových prostředků z nichž jsme uvedli pouze dva (např. je / není null, test na regulární výraz, aritmetické operátory >,<,= nebo spojování podmínek přes logické operátory and, or). Počet volání metod lze definovat přesně (např. právě jedno jako v našich testech) nebo v určitém limitu - alespoň jednou, od - do, libovolně. Umožňuje ověřovat pořadí volání konkrétních metod a naopak. Dokáže vytvořit mock objekty v různých režimech - strict mock (očekává volání jen vámi předepsaných metod - jinak generuje UnexpectedCallException), nice mock (umožňuje libovolné volání metod na tomto objektu - u neinstruovaných metod vrací "výchozí" hodnoty - null, 0 nebo false).
V druhé verzi je také novinkou Class Extension knihovna, která vám umožňuje generovat mock objekty nikoliv pouze na základě rozhraní, ale i na základě normálních tříd (tzn. není potřeba mít definovaný interface). Tento způsob vám také umožní nechat některé implementace metod mock objektů nepřekryté knihovnou EasyMock - tzn. budou obsahovat původní logiku reálné třídy.
- Pozitivní dopady mock testování
Testy jsou poměrně velmi přehledné, jednoduše se píšou a ušetří vám plno práce s nastavováním prostředí pro test. Navíc vám umožní testovat funkcionalitu, která je jen velmi obtížně testovatelná (např. že váš skript odešle v určitou chvíli email).
- Negativní dopady mock testování
Je potřeba si uvědomit, že objekty, které nahrazujete se vlastně netestují. Použití mock objektů vám umožňuje vytváření izolovaných unit testů - ty jsou sice jednodušší a přehlednější, ale nemají takovou cenu pro vlastní testování jako testy integrační. Integrační testy vám pomohou objevit chyby na úrovni rozhraní různých komponent - tzn. ne pouze, komponenta A se chová tak jak má, ale že komponenta B používá komponentu A jak má. Ve výsledku totiž uživatel aplikace nepracuje pouze s komponentami jednotlivě, ale s celým systémem komponent a chyba komunikace mezi komponentami je pro něj úplně stejná jako chyba jedné konkrétní komponenty. Prostě to nefunguje.
- Zdroje
Zdrojové kódy příkladu ke stažení jako projekt Maven 2
Mock Objects - úvod do techniky mockování
EasyMock - knihovna pro podporu dynamických mock objektů (viz. příklady v článku)
jMock - alternativa k EasyMock, abyste si mohli vybrat










01.22.2007 v 15:14
Velmi pekny clanok.
Ale mam problem s pochopenim testu c. 2. Ako to dopadne pri verify() ? Zliha to,a lebo je to OK ? Nepochopil som presne ako sa zachova konstrukcia: expectLastCall().andThrow(new SenderException) . Vysvetli mi to niekto? Vdaka.
01.22.2007 v 17:12
Všechny testy ve zdrojových kódech projdou bez chyby. V metodě verify se skutečně ověřuje pouze to, co jsem uvedl v bodě #6. Konstrukce “expectLastCall().andThrow(new SenderException())” říká přesně toto. Výsledek posledního instruovaného volání (což je v našem případě #4) bude exception, která je uvedená v parametru.
Tzn. když je zavolána objektem tested metoda performBusinessLogic (#8) a ten uvnitř svého vykonávání zavolá na messageMocku metodu send, EasyMock místo “simulace” vykonání vyhodí exception SenderException.
Obdobně by se dal mock instruovat k vrácení návratové hodnoty (pokud by metoda send měla nějakou návratovou hodnotu, jako že v našem ukázkovém příkladě nemá) deklarací (uvažujme boolean):
expectLastCall().andReturn(Boolean.TRUE)
Pokud by stále byly nějaké pochybnosti, doporučuji stáhnout si přiložený projekt a celé si to oddebugovat. Tam nejlépe poznáte, co se kdy jak volá a s jakými výsledky.
A propo. Děkuji za kladnou reakci - taková věc člověka vždycky potěší.
01.22.2007 v 19:56
Ajaj, také jsem se chystal napsat něco o EasyMock. Objevil jsem je před nedávnem a dost mi zjednodušily život.
Většinou testuji stav, ale někdy je opravdu lepší testovat procesy a na to je EasyMock jako dělaný. Našel jsem i další knihovny jako je JMock, ale přišly mi příliš komplexní pro 95% použití. A určitě musím pochválit EasyMock za žikovné použití Javy5 v v nových versích knihovny.
01.22.2007 v 20:21
[…] Před pár týdny jsem objevil skvělou knihovnu EasyMock. Chystal jsem se o ní něco málo blognout, ale Otec Fura mne předběhnul. Rozhodně si jeho post přečtěte. Nazajde moc do hloubky, ale na první dojem to stačí. […]
01.24.2007 v 15:28
zdrojaky ako png - to je pomerne velka vzacnost!
01.24.2007 v 15:53
Nicméně kompletní zdrojáky je možné si stáhnout jako ZIPko, takže pokud máte zájem o detaily, originály jsou k dispozici.
01.25.2007 v 10:19
Moc ty mock objekty nechápu. Teď mám nějaké automatické testy, které dynamicky vytvoří SQL dotaz a vyberou něco z databáze. Já pak potřebuju zjistit, jestli ten SELECT něco vrací nebo ne. Těchto testů probíhá za sebou několik a vždy se hrabe do databáze a tahá to data, což dělá testy poněkud zdlouhavými, protože je tam hodně záznamů. Dá se pro tento problém použít nějaký mock objekt, který dobu provádění zkrátí? Já si teda myslím, že ne, protože zde se jedná o kontrolu dat a ne o kontrolu přístupu k databázi. Mám pravdu?
01.25.2007 v 10:33
Mock objekty se typicky nehodí na testování “datové vrstvy” - tzn. pokud potřebujete testovat, zda jste složil správně SQL dotaz, tak v takovém případě vám nezbyde než se skutečně dotázat databáze a počkat si na reálné výsledky. S tím souvisí i nutná příprava testovacích dat.
Mock objekty se hodí na testování aplikační vrstvy (viz. příklad s business objektem). Vrstva obsahující aplikační logiku obvykle pracuje s datovou vrstvou, ze které získává data pro své operace. Za předpokladu, že máme již datovou vrstvu otestovanou a funkční - můžeme si ušetřit práci při testování aplikační vrstvy použitím mocků.
A) původní přístup:
chci testovat aplikační objekt, ten se dotazuje datové vrstvy -> před vlastním testem musím naplnit data v databázi, tak aby, když se aplikační objekt zeptá datového, vrátily správná data. Je to dost práce, ale testuju tím vlastně najednou několik věcí (integrační test): vlastní aplikační objekt, objekty datové vrstvy, spolupráci aplikačního objektu s datovou vrstvou
B) přístup s mocky:
chci testovat aplikační objekt, ten se dotazuje datové vrstvy -> v testu si vytvořím virtuální mock objekty datové vrstvy a nainstruuji jim jejich chování - tzn. řeknu jim, že až se jich aplikační objekt zeptá, mají vrátit konkrétní testová data. Žádné dotazy do databáze neproběhnou. Výsledkem je daleko méně práce při psaní testů a izolovaný (unit) test, při kterém testuji jen a jen aplikační objekt. Samozřejmě musím někde jinde otestovat zvlášť datovou vrstvu, abych zamezil chybám na tomto místě. Myšlenkou je, že i když budu izolovaně psát testy na více objektů (zvlášt aplikační a zvlášt datová vrstvaj), zabere mi to méně práce a bude to lépe otestované, než kdybych se pokoušel psát pouze integrační (složité) testy. V unit testech mám totiž větší možnosti jak otestovat i různé niance daného volání, což bych při volání ob jeden objekt obtížně testoval.