Jak na rychlé integrační testy ve Springu

Integrační testy spočívají v testování konkrétní kódu spolu s okolními částmi, se kterými spolupracuje. Cílem je snaha otestovat kód ve stavu, který se blíží reálnému nasazení. Obvykle takto testujeme datovou vrstvu aplikace (jelikož tam klasické jednotkové testy ztrácejí smysl - chceme přeci otestovat správné dotazování databáze, tudíž databázi k testu potřebujeme) a v řadě případů se nám nevyplatí mockovat ani na úrovni business vrstvy. Dokonce i Rod Johnson ve své prezentaci (kterou byl inspirován tento článek) zdůrazňuje důležitost integračních testů.

Hlavní problém integračních testů, u kterých máte ve spodu relační databázi je rychlost. Každý test spoléhá na nějaká data v DB - ty mohou být (a jsou) ostatními testy poškozena a proto je nedílnou částí všech testů setUp / tearDown operace, která se o tuto přípravu a uklizení stará. Jelikož jsou programátoři cháska líná a nechtějí se zabývat přípravou pouze minimální potřebné sady pro každý test, obvykle si vytvoří nějakého předka, který obsahuje setUp a tearDown, pro všechny testy najednou. Tím pádem se vždycky inicializuje kompletní sada dat a to si bere významné množství času. Odhadoval bych, že 80% času testů se stráví v této přípravě dat a pouze 20% času běží skutečné testy.

Na začátku projektu toto obvykle člověku nevadí, časem se to ale stane docela velkou bolestí. Na projektu, který jsem právě ukončil trvá běh testů na integračním serveru už něco kolem 30 minut. Spustit si takové testy na lokálním vývojovém prostředí je už prostě neúnosné (ještě že ty integrační servery máme ;)). Proto chce přemýšlet nad tímto problémem už od začátku - měnit princip fungování testů v pokročilé fázi projektu už stojí docela dost času.

Jedním z řešení, které například používají na rozsáhlém projektu pro bankovní sféru ve společnosti mého kolegy, je výměna datové vrstvy z cílové platformy (Oracle) za databázi v paměti (HSQL). Na začátku testů vytvoří v paměti kompletní nové schéma, které naplní daty a na konci testu celou databázi opět dropnou. Rychlost provádění se tím samozřejmě drasticky zvýší nicméně aplikaci už netestujeme na “finální” databázi, takže jsou naše testy částečně znehodnoceny. Navíc toto lze rozumně použít pouze v případě, kdy používáme ORM, který nám zakryje rozdíly mezi databázemi. V případě použití klasického JDBC nebo např. iBatisu (jako používáme my) bychom museli připravit dvě implementace a testy by už vůbec neplnily svůj smysl.

Řešení existuje!

Všechny problémy mají svá řešení a dobrá řešení se šíří jako lavina. Spring Framework je toho příkladem a i pro podporu testů obsahuje v balíku spring-mock dobré nápady. Částečně jsme toto rozkryli již v prezentaci mého kolegy základy testování ve Springu, do větší hloubky to však rozebírá sám Rod Johnson v prezentaci integrační testování ve Springu. Krom základního předka AbstractDependencyInjectionSpringContextTests (který se vám postará o načtení a zacachování spring contextu + nasetování bean přes settery do instance testu) jsou ve zmíněné knihovně ještě další třídy AbstractTranactionalSpringContextTests, AbstractTransactionalDataSourceSpringContextTests a AbstractJPATests, jejich použití si v tomto článku ukážeme.

Princip, o kterém Rod hovoří, je poměrně jednoduchý. Základem je, že máte v databázi stabilní sadu s daty, na kterých provádíte své testy. Všechny testy se mohou spolehnout na to, že v DB budou tato data a žádná jiná. Před započetím testu se AbstractTranactionalSpringContextTests postará o to, aby byla nastartovaná nová transakce, ve které test běží, a na konci je tato transakce rollbacknutá. Veškeré operace s daty, které byly v rámci testu provedeny jsou tedy vráceny zpět a další test opět běží nad stabilní datovou bází aniž bychom museli provádět nějaké extenzivní operace v setUp / tearDown.

Toto řešení má poměrně dost kladných dopadů:

  • testy se řádově zrychlí - toto zrychlení je vidět už i v případě, že spouštíme testy pouze jedné třídy
  • průměrná délka v řádcích konkrétní testové třídy se výrazně zmenší
  • vlastní psaní testů je daleko rychlejší a člověk z něj není tolik unaven - většinou se zabývá tím co chce skutečně testovat a netráví tolik času otravnou přípravou a uklízení dat

Třída AbstractTransactionalDataSourceSpringContextTests už přidává pouze několik pomocných metod, které vám umožní přistoupit k datasourcu, se kterým testy pracují a provést nad ním některé často používané operace (např. zjisti počet řádků v tabulce, vymaž všechny záznamy v tabulkách, proveď nějaký SQL příkaz).

Třída AbstractJPATests je optimalizovaná pro projekty, které používají na datové vrstvě ORM frameworky, které provádějí instrumentaci POJO a dalších potřebných tříd. Pro tento účel je vytvořen tzv. ShadowingClassLoader, který izoluje takto instrumentované třídy a je možné jej i se všemi třídami zrušit v případě potřeby (nicméně přiznám, že na tuhle oblast nejsem expert, takže vám k tomu víc neřeknu).

Příklad z praxe

V této části uvedu pár příkladových tříd z projektu, na kterém v současnosti pracuji. Vytvořil jsem si ještě jednoho předka, který mi zaručí konzistenci datové báze pro testy. Ve zkratce se tento předek před započetím transakce zeptá na jména tabulek a očekávaný počet řádků v těchto tabulkách - před startem každého testu se provádí tato jednoduchá kontrola. V případě, že počty sedí test počítá s tím, že data jsou v pořádku a test se spustí. Pokud by test nastartoval a databáze by byla prázdná, nebo by někdo ručně změnil data v databázi, došlo by k obnovení dat, se kterými test počítá. Vlastní testy běží v transakci, takže data neovlivní - ale v některých případech potřebujeme pro test toto chování vyřadit (např. potřebujeme právě otestovat správné chování transakcí) a k ovlivnění dat v DB dojde. V takovém případě se data před startem dalšího testu znovu vytvoří (tedy pokud došlo ke změně počtu řádků, pokud nikoliv může programátor zavolat metodu setDatabaseDirty a k obnovení dat si tímto vynutí). Takové případy, kdy ale potřebujeme jet mimo “testovou” transakci je ale dost málo, takže těch pár “kompletních” inicializací už nečiní takový problém, jako když se inicializace dělaly před každým testem.

Kód tohoto předka je zde:

V projektu si potom vytvářím ještě dalšího předka, který implementuje strategii obnovy dat pro projektové testy. V předku mám statické pole se seznamem POJO objektů, které reprezentují data v databázi a se kterými mohou testy dále pracovat. Tyto POJO objekty jsou v populateEmptyTables metodě vloženy do databáze.

Pozn.: Možná by bylo vhodnější data vkládat způsobem nezávislým na kódu naší aplikace kterou testujeme (tedy nikoli přes daoRole.createRole(…)), jenže tento způsob je prostě o mnoho jednodušší a přistupuji na tu nevýhodu, že pokud bude chyba v této metodě, nerozjedou se ani testy.

Vlastní testová třída již vypadá velmi jednoduše. Testy se skutečně soustředí na logiku aplikace, píšou se velmi jednoduše a na mém počítači trvá spuštění všech testů této třídy asi 3 vteřiny, z čehož přes 2 vteřiny trvá inicializace Springu a získání konekce z databáze. Když rozšířím sadu testů na pět (každý má zhruba stejný počet metod), trvá celý běh asi o 0,6 vteřiny déle. Původním způsobem s inicializací DB před každým testem bych byl minimálně na 30 sekundách.

Závěr a odkazy

Na vyzkoušení tohoto způsobu implementace testů mne upozornila již několikrát zmiňovaná přednáška Roda Johnsona o integračním testování. Přednáška má cca. 90 minut a rozhodně stojí za shlédnutí - Rod tam rozebírá ještě další věci, takže pokud už celý Spring nemáte v malíku, doporučuji.

Detailní popisky o fungování Spring test tříd jsou v javadocech, proto v závěru článku uvedu odkazy. O základech testování ve Springu přednáší kolega v prezentaci Basics of JUnit testing with Spring. Pokud vás zajímají základy, doporučuji jeho přednášku.

Odkazy:

Podělte se s ostatními:
  • Digg
  • del.icio.us
  • De.lirio.us
  • Technorati
Ohodnoťte článek:
Takovéhle články už radši ne!Nic nového pod sluncem.Průměr - obsahuje zajímavé střípky informací.Hodnotný článek - lecos nového jsem se dozvěděl.Skvělý článek - informace se mi dost hodí. (9 hlasů, průměrně: 4.89 z 5)
Loading ... Loading ...

1 reakce to “Jak na rychlé integrační testy ve Springu”

  1. Roman Dagi Pichlik:

    Pristup s rollbacknutim transakce rozjete v testu ma jeste jednu vyhodu a to, ze je mozne diky defaultni izolaci transakce bezet vice konkurentnich testu nad jednou databazi a to aniz by se testy ovlivnily. Pokud test potrebuje udelat neco, co zmeni trvale stav databaze a tudiz by doslo k nekonzistenci dat, tak by nebylo spatne poskytnout v tom predkovi kompenzacni metodu, ktera by to vratila do puvodniho stavu. Predek by ji jednak volal a jednak sam implementoval jako prazdnou, potomek by mohl v pripade potreby dodat patricne chovani.

  2. Novoj:

    No to je přesně to, co dělá AbstractDatabaseSpringTestCase (se základní implementací v AbstractProjectDatabaseTestCase), jehož kód jsem uvedl v článku. Test v takovém případě buď

    a) změní počty řádků v tabulkách - předek to detekuje a refreshne data v DB
    b) zavolá metodu setDatabaseDirty a předek v tearDown zareaguje stejně

    Nicméně řekl bych, že jakmile máš jeden jediný test, který neběží v transakci a modifikuje natvrdo data v databázi, zavírá si tím člověk vrátka ke konkurentnímu běhu více testů najednou - jistota je už ta tam.

  3. Roman Dagi Pichlik:

    Ja jsem to myslel tak, ze potomek si udela tu opravu sam. Napriklad meni pouze data jednoho radku, tak je zbytecne aby se musela znovytvaret cela tabulka pripadne cast databaze kvuli referencni integrite. Tahle kompemnzacni metoda by byla jenom volitelna.

  4. Novoj:

    JJ, jasně - tohle je výkonnější varianta. Asi jsem radikální, nechtělo se mi riskovat, že ten programátor špatně uklidí a padne mi to třeba o pět testů dál. Někdy si říkám, že než riskovat takovéhle nepříjemné chyby, je možná lepší stisknout VELKÝ ČERVENÝ KNOFLÍK :).

  5. Kamil Ševeček:

    1) Problém je v tom, že když potom ladíš test, který běží celý v transakci, nemůžeš se v půlce testu (když je kód zastaven na breakpointu) podívat na data v databázi (jestli jsou v správně).

    2) Naše projekty používají často web servicy (máme stromovou strukturu projektů, které se navzájem využívají pomoci SOAPu) a s těmi mi lokální DB transakce moc nepomůže.

  6. Novoj:

    Jasně - žádné řešení není stoprocentní. Když ladím test, zatím to pro mě problém nebyl, že jsem nevěděl co mám v tu chvíli v databázi - obvykle totiž jeden test nevygeneruje tolik nových dat, které v DB už nejsou před započetím testu.

    Zrychlení a zjednodušení testů rozhodně stojí za tuhle malou nevýhodu.

    Ad 2) v tomto případě to půjde těžko aplikovat - leda na integrační testy subsystému, schovaného za danou WS

    Spíš jde to to, že čím dál víc přicházím na to, že pokud se chce efektivně testovat, tak to obvykle jde - jen na řadu věcí přicházím o dost později, než bych potřeboval nebo chtěl ;).

  7. Pavel Jetenský:

    Zrovna teď se mi to bude moc hodit, zavádím testy do projektu, kde je potřeba připravit hodně provázaných dat a měl jsem trochu obavy, jak to udělat aby měl test při každým spuštění čistý data a aby netrval dlouho pro každou metodu.

    Takže Honzo díky :)

Nechte zde svůj komentář

Opište prosím text z obrázku: