Perzisztencia réteg tesztelése DbUnit-tal

Frissítve: 2014. január 5.

Felhasznált technológiák: DbUnit 2.4.9, Hibernate 4.3.0, HSQLDB 2.3.1, SLF4J 1.7.5, JUnit 4.11, Maven 3.0.3

Gyakran hajlamosak vagyunk a perzisztens rétegre úgy tekinteni (pl. Data Access Object - DAO J2EE tervezési minta), mint egy szükséges, magától érthetődő elemre, azonban ennek tesztelését sem szabad elfelejteni, különösen ha adatintenzív alkalmazásról, vagy egy egyszerű CRUD (create, read, update, delete) felületről van szó.

Azonban ez már nem a unit tesztelés témakörébe tartozik, hiszen itt nem magát a DAO osztály logikáját teszteljük, hanem annak hatását az adatbázisra. Így már legalább két komponens vesz részt aktívan a tesztelésben. Ezen kívül az adatbázis kapcsolatot is fel kell építeni, ami egy webes, vagy Java EE alkalmazás esetén JNDI-ből vett DataSource, vagy JPA esetén egy konténer által (alkalmazásszerver, vagy Spring) injektált EntityManager példány. Egyszerű teszteset írásakor azonban nincs sem JNDI, sem dependency injection, ezért fejlettebb eszközökhöz kell fordulnunk.

A perzisztens réteg tesztelésénél ugyanazt a módszert használjuk, mint általában a tesztelésnél, inicializáljuk az input adatokat, meghívjuk a tesztelendő kódot, majd ellenőrizzük az adatokat. Ezzel kapcsolatban több probléma is felmerül a perzisztencia esetén. Első, hogy az inicializálás jelen esetben adatbázisműveleteket jelent. Sőt, amennyiben egy klasszikus adatbázissal dolgozunk, lehetséges, hogy már vannak benne adatok, amelyek megzavarhatják a tesztet. De lehet, hogy törölni sem akarjuk őket, mert kellhetnek a teszteléshez. Második probléma a tesztelendő kód futtatásakor az előbb említett adatbázis kapcsolat kiépítése. Hiszen tesztelésnél ez teljesen másképpen történhet, mint futás közben. Harmadrészt az assert-ek megírása sem egyszerű feladat, hiszen lehetséges, hogy az adatbázis állapotát akarjuk ellenőrizni, akár több tábla több sorát.

Ezekben segít nekünk a DbUnit, mely egy JUnit-ra épülő teszteléshez használható library. Segít nekünk az adatbázis inicializálásában (ottmaradt adatok törlése, input adatok betöltése), valamint a aktuális és az elvárt adatok összehasonlításában.

Ezeket un. Helper osztályokkal és metódusokkal teszi. Definiál egy IDataSet interfészt, mely több táblát tartalmazó adathalmaz, valamint egy ITable, mely egy tábla adatai. Azonban az IDataSet interfésznek több megvalósítását is biztosítja. Egyrészt természetesen lehetséges ennek betöltése adatbázisból, de lehetőség van arra, hogy a betöltés XML állományból (két fajta: flat, original), lekérdezésből (SQL select), Excel állományból, stb. történjen. Valamint különböző metódusokat biztosít ezek összehasonlítására.

Ezek felhasználásával egy tipikus teszteset a következőképpen nézhet ki. Definiálunk XML-ben egy DataSet-et, mely a teszt esethez szükséges pár tábla input adatait tartalmazza, és beszúrjuk ezeket. Lefuttatjuk a tesztelendő metódust, majd betöltjük XML állományból az elvárt állapotot is, és összehasonlítjuk magával az adatbázissal.

A DbUnit alapvetően a JUnit 3.x szériára épül, de lehetséges a használata a 4.x verziókban is. A DbUnit JAR állományán kívül az SLF4J API és valamelyik megvalósításának (Log4J, Commons Logging, java.util.logging) JAR állományát is be kell tenni a tesztelés classpath-jába.

Készítettem is egy példa projektet, mely elérhető a GitHub-on, az "mvn test" parancs kiadásával indíthatóak a tesztesetek. Van egy Employee osztályt, melynek példányait le szeretnénk menteni és beolvasni, és egy EmployeeDao interfész, és ennek EmployeeDaoJpa implementációja.

A példában HSQLDB beépített adatbáziskezelőt használok. A JUnit 3.x széria esetén a JdbcBasedDBTestCase, DataSourceBasedDBTestCase vagy JndiBasedDBTestCase osztályokból kéne leszármaztatnuk az osztályunk, de amennyiben ez valami miatt nem lehetséges, mert mástól akarunk öröklődni, vagy JUnit 4-et használunk és nem szeretnénk leszármaztatni, elvégezhetjük az inicializációt mi magunk is. Itt hozzunk létre egy adatbázis kapcsolatot, és a JPA számára egy EntityManagerFactory-t. Majd töltsünk be egy XML állományt, mely az Employee tábla adatait tartalmazza, majd ürítsük az adatbázisból az Employee táblát, és szúrjuk be az előbb betöltött adatokat.

private static DataSource dataSource;
private static EntityManagerFactory emf;

private EntityManager entityManager;

private EmployeeDao employeeDao;

@BeforeClass
public static void init() throws Exception {
 Properties properties = new Properties();
 properties.put("url", "jdbc:hsqldb:mem:dbunittutor");
 properties.put("user", "sa");
 properties.put("password", "");

 dataSource = JDBCDataSourceFactory.createDataSource(properties);
 emf = Persistence.createEntityManagerFactory("dbunittutorPu");
}

@Before
public void setUp() throws Exception {
 IDatabaseConnection conn = 
  new DatabaseDataSourceConnection(dataSource);
 IDataSet data = new XmlDataSet(EmployeeDaoTest.class
  .getResourceAsStream("/employees.xml"));
 DatabaseOperation.CLEAN_INSERT.execute(conn, data);

 assertEquals(3, conn.getRowCount("employee"));

 entityManager = emf.createEntityManager();
 employeeDao = new EmployeeDaoJpa();
 ((EmployeeDaoJpa) employeeDao).setEm(entityManager);
}

A példában látható, hogy az adatbázis kapcsolatot egy IDatabaseConnection interfészt implementáló DatabaseDataSourceConnection osztály burkolja be, de az IDatabaseConnection interfésznek egyéb megvalósításai is vannak, pl. DatabaseConnection, melynek egy Connection példányt kell átadni.

A employees.xml írja le a kezdeti adatbázis tartalmát:

<?xml version="1.0" encoding="UTF-8"?>
 <dataset>
 <table name="employee">
  <column>id</column>
  <column>name</column>
  <row>
   <value>1</value>
   <value>name1</value>
  </row>
  ...
 </table>
</dataset>

A következő teszt eset a listEmployees() metódust teszteli.

@Test
public void testListEmployees() {
    List<Employee> employees = employeeDao.listEmployees(1, 2);
    assertEquals(2, employees.size());
    assertTrue("A name prefixszel kell kezdodnie", 
        employees.get(0).getName().startsWith("name"));
}

A következő metódus a mentést ellenőrzi, de úgy, hogy az adatbázis elvárt állapotát egy XML-ből tölti be, és összehasonlítja az adatbázis tényleges állapotával.

@Test
public void testListEmployees() {
    Employee employee = new Employee("name4");
    employeeDao.persistEmployee(employee);

    ITable tableDb = new DatabaseDataSourceConnection(ds)
        .createDataSet().getTable("employee");
    ITable tableXml = new XmlDataSet(EmployeeDaoTest.class
        .getResourceAsStream("/expectedEmployees.xml"))
        .getTable("employee");

    new DbUnitAssert().assertEquals(tableXml, tableDb);
}

Ehhez a expectedEmployees.xml már ki van egészítve a beszúrt Employee adataival.

Ez lehet, hogy elsőre tökéletesen lefut, de több probléma is van vele. Második futtatásnál már hibát jelezhet a teszteset. A hiba szövege nagyon beszédes, leírja, hogy melyik tábla hanyadik sorában van az eltérés, és kiírja az aktuális és az elvárt értéket is. A hiba ott lehet, hogy egyrészt nem biztos, hogy a sorokat a megfelelő sorrendben kapjuk vissza. Ehhez van a SortedTable osztály, mely a dekorátor tervezési mintát valósítja meg. Ezzel valamelyik oszlopra lehet rendezni. Másrészt az id kiadása is egy szekvencia alapján történik, így erre sem kell ellenőrizni. Erre használjuk a DefaultColumnFilter osztályt, mellyel meg lehet mondani, hogy mely oszlopokat akarunk figyelembe venni az összehasonlításkor. Így a javított kód a következő.

ITable tableDb = new SortedTable(DefaultColumnFilter
    .includedColumnsTable(new DatabaseDataSourceConnection(ds)
    .createDataSet().getTable("employee"), new String[]{"name"}), 
    new String[]{"name"});

ITable tableXml = new SortedTable(DefaultColumnFilter
    .includedColumnsTable(new XmlDataSet(EmployeeDaoTest.class
    .getResourceAsStream("dataset_result.xml")).getTable("employee"),
    new String[]{"name"}), new String[]{"name"});

Ez mindkét táblán szűrést és rendezést is végez, és ezután már össze lehet hasonlítani őket.

Az ITable interfésznek egyéb Helper metódusai is vannak, melyeket érdemes használni, mert megkönnyítik a programozást. Egyrészt a getRowCount() metódus visszaadja a sorok számát, másrészt a getValue(0, "name") visszaadja a 0. sor name oszlopának értékét.

A perzisztens réget tesztelésekor a következő tanácsokat érdemes betartani:

  • Minden fejlesztő kapjon saját adatbázist, vagy használjunk embedded adatbázist. Ezzel kikerülhető az, hogy egyszerre többen használják, így összekeverednek az adatok.
  • Két stratégiát választhatunk teszteléskor: vagy minden teszteset után visszagörgetünk (rollback), vagy minden teszteset előtt inicializáljuk az adatbázist. Az elsőt használják többen is, de én a másodikat javaslom, mert mikor először próbálkoztam az első megközelítéssel, a teszt esetek sikeresen lefutottak, a kivétel pont a commit műveletnél jött. Az utóbbi esetében viszont szintén érdemes megfogadni két tanácsot. Egyrészt kis adathalmazokkal dolgozzunk, hogy a teszt lefutása gyors legyen. Másrészt nem kell a teszteset lefutása után cleanup, hiszen a teszt eseteket úgy kell megírni, hogy az inicializálás (@Before) úgy állítsa be az adatbázist, hogy azon azonnal le lehessen futtatni a tesztesetet. A tesztesetek között sorrendi függőség, állapotátmenet ne legyen lehetséges.
  • Amennyiben read-only adataink is vannak, melyet minden teszt eset használ, javasolt azokat osztályszintű inicializáláskor betölteni, szintén a gyorsítás végett.

Amennyiben a DataSource JNDI-ből való lekérését is tesztelni akarjuk, használhatunk stub/mock JNDI Context implementációt.

A DbUnit Ant-ból is használható, és Maven plugin-je is van, melynek goal-jai képesek integrációs tesztelés esetén az adatbázis inicializációjára, adatbázis adatainak xml-be exportálására, valamint DataSet-ek összehasonlítására.

Érdemes még megnézni az SQLUnit projektet is, melynek érdekessége, hogy a tesztesetek XML-ben van leírva, és szintén a perzisztencia tesztelésére szolgál. Inkább javasolt pl. tárolt eljárások tesztelésére. Sajnos fejlesztését befejezték, utolsó release 2006. júniusában volt.

Spring használata esetén kötelező olvasmány a Reference Testing fejezete, különösen a tranzakciókezelés részre.

JPA esetén javasolt az Apress kiadásában készült Pro EJB 3 Java Persistence API könyv, melynek 12. fejezet a JPA teszteléséről szól. Itt a tesztelés különböző szintjeit tárgyalja. Első szint, mikor csak az entitást, mint POJO-t teszteled. Második szinten már a DAO-t teszteled, de adatbázis kapcsolat nélkül, stub/mock EntityManager-rel. Az integrációs teszt során kötöd össze a DAO-t ténylegesen az adatbázissal. Itt lehet trükközni azzal, hogy a teszteset futtatásakor honnan veszel EntityManager példányt (pl. factory), illetve felül lehet írni a teszteset futtatásakor az entitáson elhelyezett annotációkat xml konfigurációval. Különösen érzékeny terület itt is a tranzakciókezelés.