úterý 25. října 2005

Databázové scénáře pro integrační a jednotkové (unit) testy

Pokud máte aplikaci, která je výhradně závislá na databázi, a chcete psát jednotkové a nebo integrační testy, pak jste postaveni před dva problémy.

  • nastavení testovacích dat alias uvedení databáze do výchozího stavu
  • kontrola dat v databázi v rámci testu

Jednou z možností, jak se výše uvedených problémů zbavit, je použít techniku tzv. mockování. Technika mockování je založena na tom, že vlastní testovaný objekt nepracuje v runtime s instancemi závislých objektů, ale s jejich atrapami, které v sobě mají předpřipravená data.

Ač jsou mock objekty dobrým řešením, především pro jednotkové testy, mají svá omezení. Tím nejzásadnějším, alespoň z mého pohledu, je nemožnost simulovat chování komplexních procesů, které probíhají na pozadí. Typickým příkladem pro databáze je omezení referenční integritou. Je velice neefektivní si připravovat mock objekt, který bude věrně simulovat chování databáze. Samozřejmě to nemusí být přímo databáze, ale například aplikační mezivrstva v podobě Hibernate a pod ní teprve databáze.

Další alternativou k mock objektům, a tentokrát plnohodnotnou, představuje framework DbUnit. DbUnit umožňuje jednak provést nastavení testovacích dat a také jejich kontrolu. Práci s DbUnit ukazuje článek Andrew Glovera Effective Unit Trstiny with DbUnit.

DbUnit je velice užitečný nástroj a má API podporu pro nejrůznější operace přes porovnávání datasetu až po extrakci databázových schémat. Jenže, starého psa, novým kouskům nenaučíš.

Jde o to, že jsem:

  • nechtěl nutit vývojáře vytvářet testovací data jako XML
  • nechtěl nutit vývojáře zkoumat další API
  • chtěl umožnit jednoduché využívání/rozšiřování předpřipravených dat

Výsledkem je jednoduchý "framework", který jsem pojmenoval ScenarioTestCase, a který je založený na jUnit. Stručná charakteristika by mohla znít takto: slouží pro psaní integračních a jednotkových testů za použití standardního API Javy a klasického SQL, s cílem umožnit jednoduché znovupoužití/rozšíření existujících testovacích dat.

Diagram tříd ScenarioTestCase frameworku Plná velikost (cca. 38KB)

Srdcem celého díla je abstraktní třída ScenarioTestCase, která jednak řídí životní cyklus testů a umožňuje spouštění testovacích scénářů. Termín testovací scénář, kterému odpovídá rozhraní TestSQLScenario, představuje sadu SQL dotazů starajících se o inicializaci a úklid testovacích dat.

ScenarioTestCase zaručuje:

  • před spuštěním testu volání metody onStartUp
  • před každou testovací metodou exekuci všech zaregistrovaných scénářů
  • před každou testovací metodou volání onSetUp
  • po každé testovací metodou volání onTearDown
  • možnost vykonat libovolný SQL dotaz (insert, update, delete, select)

Za zmínku stojí ještě třída ConnectionscenarioTestCase, která je potomkem ScenarioTestCase. Ta umožňuje otevření databázového připojení na základě předaných parametrů a dále implementuje práci s JDBC. Celý princip psaní testu ukazuje následující příklad. Modelová situace, máme tabulku, ve které je uloženo přihlašovací jméno uživatele a nějaké další hodnoty. Výsledný testovací objekt se pak snaží ověřit funkčnost aplikační logiky, která by měla změnit přihlašovací jméno

Napsání scénáře

Testovací scénář obsahuje SQL příkazy pro inicializaci a úklid databáze. Každý testovací scénář musí implementovat již zmíněné rozhraní TestSQLScenario, které definuje dvě metody. Metoda getInitSQL slouží pro vrácení seznamu inicializačních SQL příkazů. Metoda getCleanUpSQL slouží pro vrácení úklidových příkazů.

 
public class ExampleScenario implements TestSQLScenario{
  
    public List getInitSQL() {
        ArrayList l = new ArrayList();
        l.add("insert into users (uname, user_id,) values ('pavel', 1)");
        return l;
    }

    public List getCleanUpSQL() {
        ArrayList l = new ArrayList();
        l.add("delete from users where user_id = 1979");
        return l;
    }
}
 

Vytvoření vlastního testu

Vlastní testovací objekt je napsán jako potomek třídy ConnectionscenarioTestCase . V metodě onStartUp se provede vytvoření testovacího scénáře a jeho registrace pomocí metody addScenario. Takto je samozřejmě možné registrovat libovolné množství scénářů. Třída ConnectionscenarioTestCase definuje abstraktní metodu getConnectionParametrs, pomoci které potomci specifikují parametry pro otevření databázového připojení.

 
public class ExampleTest extends ConnectionScenarioTestCase {
    
    protected String[] getConnectionParameters() {       
        return new String[]{
                "com.mysql.jdbc.Driver", 
                "jdbc:mysql://localhost/test", 
                "test", 
                "test"};
    }

  
    protected void onStartUp() {      
        super.onStartUp();
        addScenario(new ExampleScenario());//přidání scénáře        
    }

    
    public void testFoo(){       
        //zinstancování aplikační logiky
 AplikacniLogika al = new AplikacniLogika(); 
 //změna uživatelského jména
        bl.zmen('šavel')
        
        //Pro ověření použijeme select, kde předpokládanou změnu 
        //zahrneme v rámci where klauzule viz uname='šavel'.
        int count = 
jdbcSelect("select count(*) from users where uname='šavel' and user_id = 1");
        
        //V případě, že logika zafungovala správně, musí byt v proměnné count hodnota 1
        assertEquals("Uživatelské jméno musí být změněno", 1, count);
    }
    
    public void testHoo(){       
       //test something else
    }
}
 

Testovací objekt lze pouštět jako jakýkoliv jiný jUnit test.

Rozšiřitelnost

ScenarioTestCase je určen k tomu, aby byl rozšířen patřičným způsobem a ConnectionScenarioTestCase představuje ukázku, jak to lze udělat. Typické rozšíření může například představovat implementaci, kdy nastartujeme například databázový pool a nebo kontejner (Spring), ve kterém "žije" aplikační logika.

Objekty implementující rozhraní TestSQLScenario lze skládat a nebo rozšiřovat. Myšlenka je taková, že by mělo vzniknout několik základních scénářů, které se využívají přes většinu testů. Pokud je potřeba testovací scénář rozšířit, udělá se jeho potomek a nebo kompozice.

Tento jednoduchý "framework" není rozhodně zamýšlen jako konkurence nebo náhrada DbUnit. Je to pouze jednoduchoučká alternativa pro pohodlné psaní testů závislých na databázi. Zdrojové soubory a zkomipolované třídy je možné použít dle libovůle. Jenom připomínám, že je potřeba mít na classpat jUnit a že je to psané pro Javu 5.0.