Monday, March 26, 2012

Integrating DBUnit with Spring TestContext Framework

The Spring Framework 2.5 came with a new TestContext framework which made integration testing of database code a lot easier. It provided annotations to declaratively specify the application context to execute your test in, annotations to mark the transactions for test methods, and also some base test classes for JUnit and TestNG. In the following article I will describe an approach for integrating the TestContext framework with the DBUnit framework, which in turn allows you to initialize the test database before test and verify its condition with the expected dataset after the test is complete.

Here’s a simple example. We are about to test the correctness of persisting a domain object.
@Entity
public class Person {

    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    
    private String name;
...
Here’s DAO that does the saving:
public class JpaPersonDao implements PersonDao {

    @PersistenceContext
    private EntityManager em;

    public void save(Person person) {
        em.persist(person);
    }

}
It’s worth mentioning that integration testing of DAO implies testing the DAO itself in complex with domain object mappings as well as the underlying persistence provider. In our test case we shall use Hibernate. Let’s create a Spring application context named testContext.xml with the following content (headers and beans tag omitted):
    <!-- For declarative transactions via @Transactional annotation -->
    <tx:annotation-driven/>

    <!-- we’ll use embedded HSQLDB database -->
    <jdbc:embedded-database id="dataSource" />

    <!-- persistence unit configuration -->
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="persistenceProviderClass" value="org.hibernate.ejb.HibernatePersistence"/>
        <property name="dataSource" ref="dataSource"/>
        <property name="packagesToScan" value="ru.kacit.commons.test.dbunit"/> <!-- package with domain classes -->
        <property name="jpaPropertyMap">
            <map>
                <entry key="hibernate.show_sql" value="true"/>
                <entry key="hibernate.format_sql" value="true"/>
                <entry key="hibernate.hbm2ddl.auto" value="create"/>
            </map>
        </property>
    </bean>

    <!-- generic transaction manager -->
    <bean class="org.springframework.orm.jpa.JpaTransactionManager" id="transactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    
    <!-- DAO under test -->
    <bean class="ru.kacit.commons.test.dbunit.JpaPersonDao" />
Now let’s create a test class by subclassing a standard Spring TestContext Framework class for transactional tests with JUnit. The @ContextConfiguration annotation specifies the context file (in our case, it’s located on classpath) that shall host the current test. This allows for injecting the DAO under test using @Autowired annotation.
@ContextConfiguration("classpath:testContext.xml")
public class JunitDbunitTest extends AbstractTransactionalJUnit4SpringContextTests {

    @Autowired
    public PersonDao personDao;

    @Test
    public void test1() {
        personDao.save(new Person("Chip"));
        personDao.save(new Person("Dale"));
        personDao.save(new Person("Gadget"));
    }
}
The base class AbstractTransactionalJUnit4SpringContextTests is configured for running each test method in a transaction that gets rollbacked after the test is over.

Now we have to verify that the data actually got persisted into the database. We could simply inject EntityManager in our test class and use it right after persisting the data to make the asserts we need. In most cases of testing a DAO that would be enough to verify the correctness of mappings and DAO logic. The transaction will rollback after the test, and all side effects of the test will be successfully eliminated.

There are cases though when we have to commit the transaction after the test, to ensure its correct ending and to check the data that actually got saved in the database, down to specific fields of tables. It’s worth mentioning that such verifications are likely to tightly couple the test to physical structure of the database, thus increasing its sensitivity. Moreover, the test may fail when executed against another persistence provider due to differences in exported database structure or table naming.

The base test classes provided by Spring only allow you to execute SQL statements against the test database. Let’s see how DBUnit may help us here, and how to integrate it with Spring TestContext Framework.

DBUnit allows you to describe the condition of the database as a generic XML dataset, with no binding to underlying physical data types. Here’s an initial dataset for our test. It is empty, there’s a single ‘persons’ table with two fields in it. It corresponds to the domain class we’ve defined earlier.

<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
<table name="person">
    <column>id</column>
    <column>name</column>
</table>
</dataset>
Here’s the expected dataset. The ‘persons’ table contains three records.
<source lang="xml">
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="person">
        <column>id</column>
        <column>name</column>
        <row>
            <value>1</value>
            <value>Chip</value>
        </row>
        <row>
            <value>2</value>
            <value>Dale</value>
        </row>
        <row>
            <value>3</value>
            <value>Gadget</value>
        </row>
    </table>
</dataset>
There’s also a shortened notation in DBUnit in which tags correspond to table names and attributes to fields. But the full-sized format oftentimes appears to be more handy.

Let’s create an annotation for a test method that will specify the datasets to load before starting the test (‘before’ attribute) and to verify against after the test is completed (‘after’ attribute):

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DbunitDataSets {

    String before();
    
    String after();
    
}
To process this annotation, we shall extend the base test class AbstractTransactionalJUnit4SpringContextTests.
@TestExecutionListeners(
        AbstractDbunitTransactionalJUnit4SpringContextTests.DbunitTestExecutionListener.class
)
public abstract class AbstractDbunitTransactionalJUnit4SpringContextTests
        extends AbstractTransactionalJUnit4SpringContextTests {

    /** DBUnit tester */
    private IDatabaseTester databaseTester;

    /** expected dataset file name */
    private String afterDatasetFileName;

    /** method to execute after the test transaction is completed — verification */
    @AfterTransaction
    public void assertAfterTransaction() throws Exception {
        if (databaseTester == null || afterDatasetFileName == null) {
            return;
        }
        IDataSet databaseDataSet = databaseTester.getConnection().createDataSet();
        IDataSet expectedDataSet = 
            new XmlDataSet(ClassLoader.getSystemResourceAsStream(afterDatasetFileName));
        Assertion.assertEquals(expectedDataSet, databaseDataSet);
        databaseTester.onTearDown();
    }

    private static class DbunitTestExecutionListener extends AbstractTestExecutionListener {

        /** method to execute before the test method — initialization */
        public void beforeTestMethod(TestContext testContext) throws Exception {
            AbstractDbunitTransactionalJUnit4SpringContextTests testInstance = (AbstractDbunitTransactionalJUnit4SpringContextTests) testContext.getTestInstance();
            Method method = testContext.getTestMethod();

            DbunitDataSets annotation = method.getAnnotation(DbunitDataSets.class);
            if (annotation == null) {
                return;
            }

            DataSource dataSource = testContext.getApplicationContext().getBean(DataSource.class);
            IDatabaseTester databaseTester = new DataSourceDatabaseTester(dataSource);
            databaseTester.setDataSet(
                new XmlDataSet(ClassLoader.getSystemResourceAsStream(annotation.before())));
            databaseTester.onSetup();
            testInstance.databaseTester = databaseTester;
            testInstance.afterDatasetFileName = annotation.after();
        }
    }
}
The static nested class DbunitTestExecutionListener extends the AbstractExecutionListener — a part of the TestContext Framework. It is bound to the test lifecycle using the @TestExecutionListeners annotation on the test class.

Our basic test class binds to the test lifecycle using two pointcuts. The first is the DbunitTestExecutionListener#beforeTestMethod that gets executed before each test method. It checks for the @DbunitDataSets annotation on the current test method. If the annotation is present, DBUnit database tester is initialized, and the ‘before’ dataset gets loaded into the database. The ‘after’ attribute value and the database tester are saved into the fields of the test class for later use.

The second pointcut is the assertAfterTransaction() method that is marked with @AfterTransaction annotation which is also a part of TestContext framework. This annotation ensures execution of the annotated method after the transaction of the @Transactional-marked test method is completed. Here we apply our previously saved databaseTester and afterDatasetFileName to compare the database condition to the expected dataset using the DBUnit functionality.

Now let’s see what our test looks like.
@ContextConfiguration("classpath:testContext.xml")
public class JunitDbunitTest extends AbstractDbunitTransactionalJUnit4SpringContextTests {

    @Autowired
    private PersonDao personDao;

    @Test
    @Rollback(false)
    @DbunitDataSets(before = "initialDataset.xml", after = "expectedDataset.xml")
    @DirtiesContext
    public void test1() {
        personDao.save(new Person("Chip"));
        personDao.save(new Person("Dale"));
        personDao.save(new Person("Gadget"));
    }
    
}
The @Rollback(false) annotation ensures that the transaction is committed after the test. The @DirtiesContext annotation states that the Spring application context has to be recreated before starting the text test in the class. Our custom @DbunitDataSets annotation defines the names of the files containing the initial and expected DBUnit datasets.

The drawback of this approach is the need to recreate the heavy Spring application context before each test method. After the test is finished, the database is polluted not only with business data (which we can easily purge with DBUnit as well as using the AbstractTransactionalJUnit4SpringContextTests#deleteFromTables method) but also with auxiliary tables and sequences of the persistence provider. Thus, each test method has to be marked with @DirtiesContext annotation which ensures recreating the Spring context and re-exporting the database schema before each sequential test.

To skip the context refresh we could re-export the database schema in a @Before method, thus eliminating need for @DirtiesContext annotation. But I decided not to do that in the base class mostly to keep it uncoupled with Hibernate. Also, even such an aggressive database purgation would keep me unsure if I really eliminated all of the side effects of the test, like Hibernate caching.

An abstract TestNG base test class is absolutely identical to the one I’ve presented here, except for extending the AbstractTransactionalTestNGSpringContextTests class. For my own purposes I’ve extracted the DbunitTestExecutionListener into a separate class and implemented two base classes for these two test frameworks.

The source code for the article is available on GitHub: https://github.com/forketyfork/spring-dao-test-demo

2 comments:

  1. Where can i download this project with source code?

    ReplyDelete
    Replies
    1. The code is available on GitHub: https://github.com/forketyfork/spring-dao-test-demo

      Delete