Tuesday, February 15, 2011

DbUnit tests with Spring and nested transactions

In my current project we are using DbUnit to set up test data for our system tests. It's a J2EE application that uses Spring and Hibernate. When we started with DbUnit we set it up to have one xml file per test class and configured DbUnit to insert the data in the xml file in a JUnit4 @Before annotated method. But this was kind of slow as the data gets reinserted before each test case. Dbunits practice of commiting its data also meant that even if we extended springs AbstractTransactionalJUnit4SpringContextTests (ATJ4SCT) the dbunit data is not rolled back. Dbunit suggests using the Clean_Insert DatabaseOperation and to have empty table rows in the xml files for tables we dont insert data into but want to have cleared by dbunit. We tried this, but it was cumbersome to always have to put in the empty table rows in the correct order to avoid foreign key constrains on delete. And on a few occasions we got false positives from test cases that passed because an earlier test had inserted data that we had forgotten to remove in the current test.
To get around this we wanted to setup DbUnit to not commit its data, so that it would automatically be rolled back if we used Springs ATJ4SCT. To do this we created a new class extending DbUnits DatabaseDataSourceConnection.

And a new class extending java.sql.Connection that delegate all the methods to the passed in connection except for the commit method that we set to do nothing.

DataSourceUtils is a Spring class that only closes the DataSource if no one else is using it.
We can use this class in our testcode like so.

With this setup we could instruct DbUnit to insert the data before each test and not have to worry about putting in empty table statements in our DbUnit xml files. But the test-cases was of course still quite slow to run. It would be nice if we could have DbUnit insert its data before the test class starts and rolled back after all test-cases in the class have finished but still have each test-case run in isolation, in its own transaction.
This is exactly what nested transactions does. It starts a new transaction inside an existing one, that can be rolled back independently of the surrounding transaction but that still sees all data written by the outer transaction. The only problem left was how to start the transaction and insert the test data before each test-class. In a @BeforeClass static method we don't have access to the Spring context. And therefore no access to the Spring configured DataSource or TransactionManager.
But Spring has the notion of TestExecutionListeners, ATJ4SCT uses a TransactionalTestExecutionListener to start and roll back a transaction around each test-method. The TestExecutionListener interface has four methods: beforeTestMethod, afterTestMethod, beforeTestClass and afterTestClass. So by creating our own TestExecutionListener and implement the -testClass methods we will have access to the Spring context and a place to do work before and after each test-class. Our TestExecutionListener looks for a DbUnit xml file with the same name as the test class that is running, to insert as test data. Most of the code is copied from Springs TransactionalTestExecutionListener.

In our base class for DbUnit test we specified to run the tests with our new TestExecutionListener and to start a nested transaction for each test-case with the Spring @Transactional(propagation=Propagation.NESTED) annotation. The only problem left is that the Hibernate session is not cleared before the outer transaction ends. So in our base class we add a call to clear on the Hibernate session after each test-case.

Thats it. All integration tests that hits the database can now just extend this base class. If you want to have another naming convention for your DbUnit xml files than the class-name, its easy to create an annotation where you can specify the filename and look for this annotation in the DbUnitTransactionPerTestClassListener class.
Feel free to leave comments about this approach.
How do you handle testdata in your own projects?