ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Effective Unit Testing with DbUnit

by Andrew Glover
01/21/2004

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:

EMPLOYEE database table

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

EMPLOYEE sample data

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

<EMPLOYEE employee_uid='1' 
          start_date='2001-11-01'			
          first_name='Andrew' 
          ssn='xxx-xx-xxxx' 
          last_name='Glover' />

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:

<?xml version='1.0' encoding='UTF-8'?>

<dataset>
  <EMPLOYEE employee_uid='1' 
            start_date='2001-01-01'			
            first_name='Drew' ssn='000-29-2030' 
            last_name='Smith' />

  <EMPLOYEE employee_uid='2' 
            start_date='2002-04-04'			
            first_name='Nick' ssn='000-90-0000' 
            last_name='Marquiss' />

  <EMPLOYEE employee_uid='3' 
            start_date='2003-06-03'			
            first_name='Jose' ssn='000-67-0000' 
            last_name='Whitson' />
</dataset>

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.

protected IDatabaseConnection getConnection() 
  throws Exception {
  
   Class driverClass = 
     Class.forName("org.gjt.mm.mysql.Driver");

   Connection jdbcConnection = 
	 DriverManager.getConnection(
	  "jdbc:mysql://127.0.0.1/hr", "hr", "hr");
        
   return new DatabaseConnection(jdbcConnection);
}

The getDataSet() method expects the creation of an IDataSet object, which is essentially a representation of a seed file containing the XML described earlier.

protected IDataSet getDataSet() throws Exception {
   return new FlatXmlDataSet(
      new FileInputStream("hr-seed.xml"));
}

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.

protected DatabaseOperation getSetUpOperation() 
  throws Exception {
   return DatabaseOperation.REFRESH;
 }

protected DatabaseOperation getTearDownOperation() 
  throws Exception {
   return DatabaseOperation.NONE;
}

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):

public void 
   createEmployee( EmployeeValueObject emplVo )

public EmployeeValueObject 
   getEmployeeBySocialSecNum( String ssn )

public void 
   updateEmployee( EmployeeValueObject emplVo )

public void 
   deleteEmployee( EmployeeValueObject emplVo )

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:

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
  <EMPLOYEE employee_uid='1' 
            start_date='2001-01-01'			
            first_name='Drew' ssn='333-29-9999' 
            last_name='Smith' />
  <EMPLOYEE employee_uid='2' 
            start_date='2002-04-04'			
            first_name='Nick' ssn='222-90-1111' 
            last_name='Marquiss' />
  <EMPLOYEE employee_uid='3' 
            start_date='2003-06-03'			
            first_name='Jose' ssn='111-67-2222' 
            last_name='Whitson' />
</dataset>

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."

public void testFindBySSN() throws Exception{

  EmployeeFacade facade = //obtain somehow
		
  EmployeeValueObject vo = 
    facade.getEmployeeBySocialSecNum("333-29-9999");

  TestCase.assertNotNull("vo shouldn't be null", vo);
  TestCase.assertEquals("should be Drew", 
     "Drew", vo.getFirstName());
  TestCase.assertEquals("should be Smith", 
     "Smith", vo.getLastName());
}

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.

public void testEmployeeCreate() throws Exception{
  EmployeeValueObject empVo = 
       new EmployeeValueObject();
  empVo.setFirstName("Noah");
  empVo.setLastName("Awan");
  empVo.setSSN("564-55-5555");
		
  EmployeeFacade empFacade = //obtain from somewhere
  empFacade.createEmployee(empVo);
  
  //perform a find by ssn to ensure existence

}

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.

public void testUpdateEmployee() throws Exception{
  EmployeeFacade facade = //obtain façade

  EmployeeValueObject vo = 
   facade.getEmployeeBySocialSecNum("111-67-2222");
		
  TestCase.assertNotNull("vo was null", vo);
  TestCase.assertEquals("first name should be Jose", 
	"Jose", vo.getFirstName());

  vo.setFirstName("Ramon");
	
  facade.updateEmployee(vo);
		
  EmployeeValueObject newVo = 
   facade.getEmployeeBySocialSecNum("111-67-2222");
  TestCase.assertNotNull("vo was null", newVo);

  TestCase.assertEquals("name should be Ramon", 
   "Ramon", newVo.getFirstName());
}

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.

public void testDeleteEmployee() throws Exception{
  EmployeeFacade facade = //obtain façade

  EmployeeValueObject vo = 
	facade.getEmployeeBySocialSecNum("222-90-1111");
		
  TestCase.assertNotNull("vo was null", vo);
			
  facade.deleteEmployee(vo);
	
  try{	
     EmployeeValueObject newVo = 
      facade.getEmployeeBySocialSecNum("222-90-1111");
     
     TestCase.fail("returned removed employee");
     
     }catch(Exception e){
      //ignore
     }
}

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:

<junit printsummary="yes" haltonfailure="yes">
  <formatter type="xml"/>
  <batchtest fork="yes" 
           todir="${reports.tests}">
    <fileset dir="${src.tests}">
      <include name="**/*Test.java"/>
    </fileset>
  </batchtest>
</junit>

With DbUnit's task, controlling the state of the database before and after the JUnit task involves creating a "setup" operation, in which the seed file's contents are inserted into a target database:

<taskdef name="dbunit" 
    classname="org.dbunit.ant.DbUnitTask"/>
<dbunit driver=" org.gjt.mm.mysql.Driver "
        url=" jdbc:mysql://127.0.0.1/hr "
        userid="hr"
        password="hr">
    <operation type="INSERT" 
            src="seedFile.xml"/>
</dbunit>

And a "tear down" operation, in which the same data is deleted from the target database:

<dbunit driver=" org.gjt.mm.mysql.Driver "
        url=" jdbc:mysql://127.0.0.1/hr "
        userid="hr"
        password="hr">
    <operation type="DELETE" 
           src="seedFile.xml"/>
</dbunit>

Wrapping the JUnit task with the above operations effectively loads the target database before the batch test executes and then deletes all loaded data when the tests complete.

<taskdef name="dbunit" 
         classname="org.dbunit.ant.DbUnitTask"/>

<!-- set up operation -->
<dbunit driver=" org.gjt.mm.mysql.Driver "
        url=" jdbc:mysql://127.0.0.1/hr "
        userid="hr"
        password="hr">
    <operation type="INSERT" 
          src="seedFile.xml"/>
</dbunit>

<!-- run all tests in the source tree -->
<junit printsummary="yes" haltonfailure="yes">
  <formatter type="xml"/>
  <batchtest fork="yes" todir="${reports.tests}">
    <fileset dir="${src.tests}">
      <include name="**/*Test*.java"/>
    </fileset>
  </batchtest>
</junit>

<!-- tear down operation -->
<dbunit driver=" org.gjt.mm.mysql.Driver "
        url=" jdbc:mysql://127.0.0.1/hr "
        userid="hr"
        password="hr">
    <operation type="DELETE" 
          src="seedFile.xml"/>
</dbunit>

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.

Andrew Glover is the founder and CTO of Vanward Technologies, a company specializing in building automated testing frameworks.


Return to ONJava.com.