Skip to main content

Control your test-environment with DbUnit and Anthill

Learn how DbUnit can ease your testing and build process

Philippe Girolami (philippe.girolami@digiplug.com), Senior Software Developer, Digiplug
Philippe Girolami was born in Paris, France. He holds an engineering degree from Ecole Centrale de Lille, France, and a Master of Science from the University of Texas at Austin. He is currently heading the development effort of Digiplug's core content management, delivery, and provisioning platform, delivering fine-tuned content to mobile phones across all formats, delivery channels, and terminal capabilities. He was also a developer at Ericsson's I-Lab in Paris and participated in the development of Ericsson's multi-terminal portal offering, a carrier-grade J2EE server, and mobile group-communication projects.

Summary:  The inception of the Extreme Programming methodology has brought test-driven development and continuous integration into mainstream Java development practices. Applying these techniques to Java server-side development can quickly become a nightmare if you don't have the right tools. In this article, software developer Philippe Girolami describes how to deal with continuous integration and how to use DbUnit in conjunction with JUnit to control the test environment end-to-end by setting up the state of the database before each test.

Date:  13 Apr 2004
Level:  Intermediate
Activity:  2212 views
Comments:  

One of the most important software practices is testing. Extreme Programming (XP) has pushed this logic to its limit by recommending test-first development and continuous integration, where tests are automatically run as often as possible. However, most non-XP shops practice testing in some form, whether they call it non-regression testing, blackbox testing, functional testing, or another name. A lot of projects use a relational database to store data, therefore any testing strategy needs to take into account what happens to the database during each test: If a test leaves a test database in an inconsistent state, all further tests are likely to fail! One way around this is to set up the database state to a known, coherent state before running each test. In this article, I will explain how our team achieved this using DbUnit together with JUnit and how we used Anthill to automate test report generation. Although it may seem like a costly setup, it actually isn't, and has proved a valuable tool.

Representing the contents of your database

DbUnit is a JUnit extension that puts your database into a known state between test runs, helping you avoid the problems that can occur when one test corrupts the database and causes subsequent tests to fail or give faulty results. It can read the contents of a table and store it to XML using a FlatXmlDataSet, as shown in Listing 1:


Listing 1. FlatXmlDataSet example
<dataset>
  <OPERATOR 
      ID='APC (Washington/Baltimore)' 
      CODE='ABC5APC'
      ENCODED_STRING='aabbcc'/>
  <OPERATOR 
      ID='ASA Ritabell'
      CODE='ABC6ASA R'
      ENCODED_STRING='bbccdd'/>
  <OPERATOR 
      ID='Advanced Info. Service PLC' 
      CODE='ABC1Adva' 
      ENCODED_STRING='ccddee'/>
  <OPE_OPERATOR 
      ID='Aerial Communications Inc.' 
      CODE='ABC2Aeri' 
      ENCODED_STRING='ddeeff'/>
</dataset>

This dataset represents the three columns of the database table named OPE_OPERATOR, which are described by the last three lines in Table 1:


Table 1. The table definition for the data in Listing 1
OPE_OPERATOR
IDINT
CODEVARCHAR
ENCODED_STRINGVARCHAR

Each XML entity identifies a table in the database, while each attribute represents the value for a column.

Setting up DbUnit in your project

Setting up DbUnit is simple. See the Resources section for information on downloading the project files. You can add all three JAR files to your project's build target for tests.

If you work in a multi-schema environment, you will want to set the DbUnit.qualified.table.names property to true. This is often the case for development teams using Oracle: Each user has his own schema. This will save you from prefixing every table name with the schema name and allows you to share test data across the team.


Querying the contents of a table

DbUnit allows you to easily execute JDBC queries and retrieve values from them. There are several reasons why you might use the DbUnit JDBC wrapper rather than plain JDBC:

  • You can create a Dataset from a SQL query and use DbUnit's assertion methods (described below).

  • You can create a Dataset from a SQL query and save it to a FlatXmlDataSet. You can then reload into the database at a later time.

  • You will be able to easily retrieve the contents of a column from any of those rows without going through an iteration.

The code in Listing 2 creates a result ITable that contains the result of the query. After I check that the row count is 1, I then check that for the first row (counting from 0) the FK_OTHER_ID column contains the number 1234.


Listing 2. DbUnit's querying functionality
String query = "SELECT * FROM MEDIA WHERE ID= "+id;
ITable databaseData = 
dbConnection.createQueryTable("EXPECTED_DATA",query);

assertEquals(1, databaseData.getRowCount());

BigDecimal foreignKey = (BigDecimal) databaseData.getValue(0, 
"FK_OTHER_ID");
assertEquals(new BigDecimal(1234)), foreignKey);


Using assert methods to check database contents

DbUnit has assertions, such as those shown in Listing 3, that you can use to compare two sets of data or two representations of a table. You will typically use these if you want to check the exact contents of a table after running a test rather than running multiple queries.


Listing 3. DbUnit's additional assertion methods
public static void assertEquals(ITable expected, ITable actual);
public static void assertEquals(IDataSet expected, IDataSet actual);


Creating data

Depending on the size of your database, how stable the schema is, and how far along in development you are, you may want to create your test data from scratch or extract it from a copy of a production database. Listing 4 shows an example of how to extract the contents from a pre-existing database (the getConnection() method can be found in Listing 6):


Listing 4. Creating FlatXmlDataSets from an existing database
public void extractTables(String targetDirectory,String[] tableNames)
    throws Exception {
    IDatabaseConnection connection = getConnection();

    for (int i = 0; i < tableNames.length; i++) {
        String tableName = tableNames[i];

        IDataSet partialDataSet = connection.createDataSet
                                   (new String[] { tableName });
        FlatXmlDataSet.write
            (partialDataSet, new FileOutputStream
              (targetDirectory + "/" + tableName + ".xml"));
    }
}

If you export a full production database, you will probably have to remove excessive rows -- or in this case use a query instead of creating a data set directly from the connection. The extract itself may be problematic for really big tables -- our team had to extract only parts of some tables using a query. Removing rows from the tables has its issues, too, mainly related to the difficulty in navigating all the foreign keys and making sure the data is consistent.


Adding test data

Adding test data can be tedious at times. Our experience has been that after an initial stage where we had trouble adding data correctly, we reached a plateau where not only did it become easy, but our understanding of the database structure improved dramatically.

Even though we were using Enterprise JavaBeans (EJB) technology to hide the database, this first-hand knowledge was still very handy. Debugging was made easier because developers understood the database better and could therefore check its contents faster. This in turn helped us greatly when refactoring the code and the database.


Using DbUnit and JUnit to create a base class

Good JUnit practice encourages developers to extend the base TestCase class to specialize behavior. DbUnit provides its own specialization, DatabaseTestCase, which you can then specialize to fit your needs.

First, create a basic test case called ProjectDatabaseTestCase to add utility methods to, as shown in Listing 5. Then, redefine setUp() and teardown() so they gracefully create and destroy a connection to the database through DbUnit.


Listing 5. Base class definition and basic methods for database setup
public class ProjectDatabaseTestCase extends DatabaseTestCase 
{        
     /** Use this connection to perform database setup */    
     protected IDatabaseConnection connection;        

     public DatabaseTestCase (String s) 
     {        
          super(s);          
     }    
     
     protected void setUp() throws Exception 
     {        
          super.setUp();        
          connection = getDbUnitConnection();    
     }    

     protected void tearDown() throws Exception 
     {        
          connection.close();        
          super.tearDown();    
     }
}

Listing 6 shows various methods used inside the class by the preceding methods:


Listing 6. Various utility methods
/**     
 * This method returns a DbUnit database connection    
 * based on the schema name     
 */    
 private IDatabaseConnection getDbUnitConnection() throws Exception  
 {                
    IDatabaseConnection connection = new DatabaseConnection (getJDBCConnection(), getSchemaName());
    return connection;    
 }
   
 private IDataSet getFlatXmlDataSet(String tableName) throws Exception 
 {        
    URL url = DatabaseTestCase.class.getResource( "/"+ tableName + ".xml");        
    if (url == null)            
       throw new Exception("could not find file for " + tableName);
      
    File file = new File(url.getPath());
    return new FlatXmlDataSet(file);
 }
   
 /** Implement yourself */
 private Connection getJDBCConnection() throws Exception 
 {
    /* Get your JDBC connection through a data source of JDBC itself */
 }
 * Implement yourself */
 private Connection getSchemaName() throws Exception 
 {
 }

Some considerations about the code above:

  • The getJDBCConnection() method is not shown, as its implementation depends on how you wish to obtain the JDBC connection: either through an application server's JNDI tree if its DataSource is Serializable, or directly using JDBC.

  • The getDbUnitConnection() method returns a connection to the database for DbUnit. The constructor for DbUnit's DatabaseConnection can take a schema name. That way, you don't have to prefix all the table names with the schema name.

  • The getFlatXmlDataSet() method creates a DbUnit dataset from the contents of an XML file located on the classpath.

Finally, it's time to actually insert the data into the test tables. DbUnit allows for various database operations, of which I used two:

  • DELETE_ALL, which deletes all the rows of a table
  • CLEAN_INSERT, which deletes all the rows of a table and inserts the rows from the dataset provided

The following four methods in the ProjectDatabaseTestCase are all you need:

  • insertFileIntoDb(): Inserts a file in the database
  • emptyTable(): Cleans a database table
  • insertAllFilesIntoDb(): Inserts all the files for your project
  • emptyAllTables(): Cleans all the tables for your project

Listing 7 shows these methods in action:


Listing 7. Methods used by underlying tests to set up the database
/** A method to insert all tables into the database.    
 *  Specify all tables to be inserted    
 */    
 protected void insertAllFilesIntoDb()  throws Exception 
 {            
    insertFileIntoDb("PRODUCT");     
    (...)     
    insertFileIntoDb("ACCOUNT");        
 }         
    
 /**      
  * This method inserts the contents of a FlatXmlDataSet file     
  * into the connection     
  */    
  protected void insertFileIntoDb(String tableName) throws Exception 
  {        
     DatabaseOperation.CLEAN_INSERT.execute(connection,getFlatXmlDataSet(tableName));    
  }           
     
  /** Empty a table */    
  protected void emptyTable(String tableName) throws Exception 
  {        
     IDataSet dataSet = new DefaultDataSet(new DefaultTable(tableName));
     DatabaseOperation.DELETE_ALL.execute(connection, dataSet);    
  }    
     
  /** Empty all the tables from the database    */
  protected void emptyAllTables() throws Exception 
  {        
     emptyTable("ACCOUNT");        
     (...)      
     emptyTable("PRODUCT");    
  }


Putting it all together

Once the base class is in place, it is easy to use DbUnit to set up the database cleanly, execute a method, and check the return value, as shown in Listing 8:


Listing 8. Putting it all together in a real test case
public void setUp() throws Exception    
{        
   super.setUp();        
   emptyAllTables();        
   service = Service.getInstance();    
}    
    
public void testFindProductByPrimaryKey() throws Exception     
{        
   insertFileIntoDb("PRODUCT");                
   ProductDTO productDTO = service.findProductByPrimaryKey(new Integer(12));        
   assertNotNull(productDTO);
   assertEquals("product Name", productDTO.getName());
}

public void testCreateAProduct() throws Exception     
{        
   service.createProduct("newly created product name");

   String query = "SELECT * FROM PRODUCT";
   ITable databaseData = dbConnection.createQueryTable("EXPECTED_DATA",query);
   assertEquals(1, databaseData.getRowCount());
   String productName = (String) databaseData.getValue(0, "NAME");
   assertEquals("newly created product name", productName);
}

In this test, I empty the database, insert the contents for one table, and check that the finder method used to find a product by its primary key works correctly by checking that it returns an element with the correct attribute. I then test the object creation works and verify the contents of the database using DbUnit's query facilities.

One important thing to notice here is how the cleaning of the database takes place in the test setup and not at the end. I do not want to depend on each test finishing cleanly.

Things to watch out for when inserting data

Database integrity constraints force you to insert or delete data in a given order. When you write your insertAllFiles() and emptyAllTables() methods, you will find that the order is not indiscriminate and is in fact imposed by your integrity constraints.

Another potential pitfall is that some columns may appear not to be inserted. This is almost always because the first row in your FlatXmlDataSet has a missing column. It seems like DbUnit then fails to recognize this column in all other rows. For example, inserting the dataset defined by Listing 9 results in table ACC_ACCOUNT containing two rows whose only non-null columns will be the primary key PK_ACC_ID:


Listing 9. Column NAME of ACCOUNT's second row will be missing
<dataset>
   <ACCOUNT ID='1' />  
   <ACCOUNT ID='2' NAME='first name' />
</dataset>

Always make sure your first row description contains all the table's columns. If you need to insert a NULL value, make that row the second row, as in Listing 10:


Listing 10. Column NAME of ACCOUNT's second row will not be missing
<dataset>
   <ACCOUNT ID='2' NAME='first name' />
   <ACCOUNT ID='1' />
</dataset>


Organizing your test data

DbUnit allows you to store your XML datasets in files. It even allows you to store a whole database in one file. Listing 11 shows the contents of the tables ACCOUNT and MEDIA:


Listing 11. A FlatXmlDataSet example for two tables
<dataset>
   <ACCOUNT NAME='first name' />
   <ACCOUNT NAME='second name' />
   
   <MEDIA ID='123' />
   <MEDIA ID='234' />
</dataset>

Deciding how you will store your test data is important. Will you store the contents of each table in a separate file or will you store all the rows of all the tables related to your system's main entities in one file? Neither is a silver bullet.

In the first case, it is more difficult to ensure data consistency across tables, but is easier to create queries from a database that already exists. In the second case, it is easy to create test sets for each test, but the fact that most systems aren't designed around one main entity makes it all but impractical. Our approach was to have one file per table.

Continuous integration with Anthill

Anthill is a free automated build tool (see Resources) that schedules your builds and publishes the results, helping XP-versed teams practice continuous integration. A build consists of checking out the source from a version control tool such as CVS, running a build script, publishing the results, and notifying the users of the results. It neatly integrates with ANT, allowing you to reuse your usual build scripts.

Running your testsuite in Anthill and reporting the results

Continuous integration has been recommended by XP experts as a way to ensure that integration woes are mitigated: By integrating all the code often enough, you are making sure that it is easy to trace problems back to their source. Integrating can be a very time-consuming task -- checking out, building, and deploying the code, then running the acceptance tests. Luckily, most of this can be automated using tools such as Anthill or CruiseControl. If you have not yet automated your build process (using Ant, for example), you should. If your build process is automated, you should add a test section to the build. If you're a diehard XP user, those should be your acceptance tests. If you're like us, those will be all the tests you have written -- whether unit, acceptance, or anything else.

Our build process was based on Ant and scheduled using Anthill. The main challenge for us was to make Anthill report the test failures and still publish the test results. The catch with Anthill is that if the build script fails, the publish script doesn't get executed, in which case you have no way of making the test report available to the developers. Our way around this was to make Anthill fail at the end of the publish script by making it check whether a property is true or false.

Targets for running your tests

The following is a quick rundown of how we ran our tests. We used the batchtest method, but any method would work. The key points are:

  • The test must be forked to work correctly with classpaths containing XML parsers in JDK 1.3.
  • The testsuite.error and testsuite.failure properties must be set to true if an error or a failure occurs. If not, they are left untouched.

Listing 12 shows an example of how to run all the tests of a specific module:


Listing 12. Running the tests for one module
<target name="test-common">
   <mkdir dir = "${project.reports}/common"/>
   
   <junit fork="true" errorproperty="testsuite.error" failureproperty="testsuite.failure">
      <classpath>
         <pathelement location="${out.classes.dir}"/>
         <fileset dir = "${shared.lib.dir}">
            <patternset refid="necessary.jars"/>
         </fileset>
      </classpath>
      
      <formatter type="xml"/>
      <batchtest todir="${project.reports}/common">
         <fileset dir="${out.src.dir}">
            <include name="**/Test*.java"/>     
         </fileset>
      </batchtest>
    </junit>
</target>

Making the test results available to the publish script

Listing 13 shows how we ran all of our tests in our build process:


Listing 13. Snippet of build.xml: Running all the tests and setting the results
<target name = "all-tests" depends = "test-module1,test-module2">
   <property name="testsuite.error" value="false"/>
   <property name="testsuite.failure" value="false"/>
   <propertyfile file="${deployDir}/tests.results">
      <entry key="testsuite.error" value="${testsuite.error}"/>
      <entry key="testsuite.failure"
value="${testsuite.failure}"/>
   </propertyfile>
</target>

An important Ant trick is to know that it sets a property's value only if it does not already have a value. So when you run every test target in turn, the testsuite.error and testsuite.failure properties can be true only if an error or a failure occurred.

The difficulty here is to be able to report the outcome of the test script to the main Ant script. Unfortunately, this is not trivial because those are two separate Ant build files in Anthill's process and you can't pass such parameters between build scripts in Ant. However, there is a "simple" solution: Save the outcome of the tests to a file, which the publish script then reads.

Using the same Ant trick, Listing 13 shows how to use the <property> command to make sure the testsuite.error and testsuite.failure properties always have a value at the end of the test script and how to save it to a file.

Making the publish script fail at the end if the tests failed

Use Listing 14 to have the publish script fail if any test failed. It's simply a matter of checking whether each of the properties saved in the build script is true.


Listing 14. Making the publish script fail if there were errors or failures
<condition property="must.fail">  
  <or>    
    <istrue value="${testsuite.error}"/>
    <istrue value="${testsuite.failure}"/> 
  </or>
</condition>

<fail message="Tests didn't run 100%. Check the log and make 
necessary changes!" if="must.fail"/>


Conclusion

Our team successfully introduced DbUnit and Anthill at the beginning of 2003. Since then, we've written and automated over a thousand tests -- 75 percent of which involve setting up the database state. We run these tests every hour and plan to run them even more often very soon. They have caught unexpected bugs many times, making them indispensable tools.


Resources

About the author

Philippe Girolami was born in Paris, France. He holds an engineering degree from Ecole Centrale de Lille, France, and a Master of Science from the University of Texas at Austin. He is currently heading the development effort of Digiplug's core content management, delivery, and provisioning platform, delivering fine-tuned content to mobile phones across all formats, delivery channels, and terminal capabilities. He was also a developer at Ericsson's I-Lab in Paris and participated in the development of Ericsson's multi-terminal portal offering, a carrier-grade J2EE server, and mobile group-communication projects.

Comments



Trademarks

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=10934
ArticleTitle=Control your test-environment with DbUnit and Anthill
publish-date=04132004
author1-email=philippe.girolami@digiplug.com
author1-email-cc=

My developerWorks community

You tell us!

developerWorks wants to get to know you

How does the developerWorks community use social media?


Special offers