Effective Unit Testing with DbUnit

Introducing DbUnit

Writing unit and component tests for objects with external dependencies, such as
databases or other objects, can prove arduous, as those dependencies may hinder isolation.
Ultimately, effective white-box tests isolate an object by controlling outside dependencies,
so as to manipulate its state or associated behavior.

Utilizing mock objects or stubs is one
strategy for controlling outside dependencies. Stubbing out associated database access classes, such as those found in JDBC, can be highly effective; however, the mock object solution may not be possible in application
frameworks where the underlying database access objects may be hidden, such as those
utilizing EJBs with container-managed persistence (CMP) or Java Data Objects (JDO).

The open source DbUnit framework, created
by Manuel Laflamme, provides an elegant solution for controlling a database dependency
within applications by allowing developers to manage the state of a database throughout
a test. With DbUnit, a database can be seeded with a desired data set before a test;
moreover, at the completion of the test, the database can be placed back into its pre-test state.

Automated tests are a critical facet of most successful software projects. DbUnit allows
developers to create test cases that control the state of a database during their life cycles;
consequently, those test cases are easily automatable, as they do not require manual intervention
between tests; nor do they entail manual interpretation of results.

Getting Started

The first step in configuring DbUnit involves the generation of a database schema definition
file. This file is an XML representation of database tables and the data found in them.

For example, a database table EMPLOYEE would be described in SQL as follows:

Moreover, a sample data set found in EMPLOYEE could be:

DbUnit's representation of the table and the sample data in XML would then become:

This generated XML file becomes the sample template for any seed files utilized
in an application.

Creating multiple seed files for associated test scenarios can be an effective strategy, as
one can segregate database states via the different database files. The multiple-seed-file
strategy allows for the creation of succinct, targeted data files for specific database tables,
rather than the database as a whole.

To seed the target database with three different employees, the XML representation would be
as follows:

With DbUnit configured to work with the desired database schema, developers have two
options for employing DbUnit in testing: in code or through ant.

DbUnit in Code

The DbUnit framework provides a base abstract test-case class,
which extends JUnit's TestCase and is called
DatabaseTestCase. Think of this class as a template pattern for which one must provide
implementations of two hook methods: getConnection() and getDataSet().

The getConnection() method expects the creation of an
IDatabaseConnection object, which wraps a normal JDBC connection.
For example, the code below demonstrates the creation of an IDatabaseConnection for a
MySQL database.

With those two methods defined, DbUnit can function with default behavior;
however, the DatabaseTestCase class provides two fixture methods that
control the state of the database before and after a
test: getSetUpOperation() and getTearDownOperation().

An effective strategy is to have the getSetUpOperation() perform a
REFRESH operation, which updates the desired database with the data
found in the seed file. Consequently, the getTearDownOperation()
performs a NONE operation.

Another effective approach is to have the getSetUpOperation() method perform
a CLEAN_INSERT, which deletes all data found in tables specified in the
seed file and then inserts the file's data. This tactic provides precision
control of a database.

Code Example

In a J2EE human resources application, we would like to
automate a series of test cases for a Session Façade that handles
employee creation, retrieval, updating, and deletion. The remote
interface contains the following business methods
(the throws clauses are removed for brevity's sake):

Testing the getEmployeeBySocialSecNum() method would require
seeding the database with an employee record. Additionally, testing
the deleteEmployee() and updateEmployee() would also
depend on a previously created database record. Lastly, the test suite
will create an employee from scratch, verifying that no exceptions were generated,
by utilizing the createEmployee() method.

The following DbUnit seed file, named employee-hr-seed.xml, will be utilized:

The test suite, EmployeeSessionFacadeTest, will extend
DbUnit's DatabaseTestCase and provide implementations
for both the getConnection() and getDataSet()
methods, where the getConnection() method obtains a
connection to the same database instance the EJB container
is utilizing, and the getDataSet() method reads in the
above employee-hr-seed.xml file.

The test methods are quite simple, as DbUnit handles the complex database
lifecycle tasks for us. To test the getEmployeeBySocialSecNum()
method, simply pass in a social security number from the seed file,
such as "333-29-9999."

Ensuring the façade's create method works properly is as easy as
executing a create operation and verifying that no exceptions were thrown.
Additionally, the next step could be to attempt a find operation on
the newly created entity.

Testing updateEmployee() involves four steps.
First find the desired employee entity, and then update the object.
Next, re-find the same entity and test to ensure that the updated
values are properly reflected.

Guaranteeing the façade's deletion function properly works is similar to
the testUpdateEmployee() method, as there are three steps:
find an existing entity, remove it, and then attempt to find it again,
verifying that no entity could be found.

The test suite code is simple and easy to follow, as the
code focuses on testing the desired object and not on any assorted
plumbing code to facilitate the test. Additionally, the test case is
easily automated.

DbUnit in Ant

Rather than extending DbUnit's DatabaseTestCase, the DbUnit framework comes with
an ant task, which allows the control of a database within an Ant build file. The task is quite powerful, as it provides a simplistic declarative strategy
for test cases. For example, running JUnit tests in Ant is as easy as
defining a task as follows:

Conclusion

The DbUnit framework's ability to manage the state of a database
throughout a test's lifecycle enables rapid test-case creation and
adoption; furthermore, by controlling a major dependency, tests that
utilize the DbUnit framework are easily automated.

DbUnit's elegant design makes learning how to properly utilize its features a
breeze. Once it's in place as a part of an effective testing strategy,
overall code stability will increase dramatically, along with the collective
confidence of your development team.