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:

  1. 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.
  2. 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.

BusinessObject zdrojový kód

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.

IMessageSender - zdrojový kód

ILogger zdrojový kód

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ří).

Test1 zdrojový kód

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.

Test2 zdrojový kód

#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 ;)