Testing Spring - Hibernate applications with Unitils

This article demonstrates a brand new open source library for unit testing, called Unitils. Unitils helps you in writing simple and maintainable unit tests with JUnit or TestNG. It glues together some widely used test libraries like DbUnit and EasyMock and offers integration with Spring and Hibernate. Unitils encourages applying good practices and unit testing guidelines. The ideas behind the code are based on the authors' concrete experience on enterprise projects.

Unitils offers following features

  • Equality assertion through reflection, with options like ignoring Java default/null values and ignoring order of collections

  • Support for database testing involving test data management with DbUnit, automatic maintenance of unit test databases and automatic constraints disabling

  • Hibernate integration features such as session management and testing the mapping with the database

  • Integration with Spring, involving ApplicationContext management and injection of spring managed beans

  • Integration with EasyMock and injection of mocks into other objects

In what follows, we demonstrate some of Unitils' most powerful features with a simple example: a test for a DAO method that uses Hibernate to connect to a relational database.


The example

Suppose we have a Phonebook interface for which we've written an implementation using Hibernate and Spring's HibernateDaoSupport. The Phonebook interface has a method searchByLastName and is implementated as follows:

public class HibernatePhonebook extends HibernateDaoSupport implements Phonebook {

    public List<Person> searchByLastName(String lastName) {
        return (List<Person>) getHibernateTemplate().executeFind(new HibernateCallback() {
                public Object doInHibernate(Session session) throws HibernateException, SQLException {
                    Criteria personCriteria = session.createCriteria(Person.class);
                    personCriteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
                    personCriteria.add(Restrictions.like("lastName", criteria.getLastName(), MatchMode.ANYWHERE));
                    return personCriteria.list();
                }
        });
    }
}

We've written following test for this method:

@SpringApplicationContext({"classpath:services-config.xml", "classpath:test-ds-config.xml"})
public class UnitilsHibernatePhonebookTest extends UnitilsJUnit3 {
    
    @SpringBeanByType
    private Phonebook phonebook;

    @DataSet
    public void testSearchByLastName() {
        List<Person> result = phonebook.searchByLastName("Doe"));        
        ReflectionAssert.assertPropertyLenientEquals("firstName", Arrays.asList("John", "Jane"), result);
    }
}

This example test seems to be a plain old JUnit test. But when it's executed, a lot of magic happens behind the scenes. This 'magic' is in fact the Unitils framework that hooks in to the execution of the test and inspects the test for annotations that ask for extra behavior to be performed. Let's take a look at the lines of code and annotations of interest and explain their effect.

Unitils-enabling the test

First of all, the class definition states:

public class UnitilsHibernatePhonebookTest extends UnitilsJUnit3 {

Extending from UnitilsJUnit3 is the simplest way to make a test Unitils-enabled. It not only makes this class a JUnit3 test (in fact, UnitilsJunit3 is a subclass of junit.framework.TestCase), but it also contains logic that enables Unitils to hook up into the execution of a unit test. For JUnit4 and TestNG, Unitils offers similar base classes called UnitilsJUnit4 and UnitilsTestNG, respectively.

Creating an application context and retrieving a Spring bean

The UnitilsHibernatePhonebookTest class is annotated like this:

@SpringApplicationContext({"classpath:services-config.xml", "classpath:test-ds-config.xml"})

The @SpringApplicationContext annotation instructs Unitils to create a Spring ApplicationContext, in which the services-config.xml and test-ds-config.xml files are loaded. This enables us to make Spring wired beans available for testing in a simple way:

@SpringBeanByType
private Phonebook phonebook;

By annotating the phonebook field with @SpringBeanByType, we tell the system to find a bean of type PhoneBook in the application context and inject it into the annotated field. Setter injection is also supported: simply move the annotation to a setter with the matching type. If you prefer injection by name, you can use either @SpringBean or @SpringBeanByName. When using @SpringBean, you have to specify the bean's name explicitly; in case of @SpringBeanByName, the name of the target setter or field is used.

A similar functionality can by achieved by extending Spring's own AbstractDependencyInjectionSpringContextTests. But the strength of Unitils is that it goes further, integrating with other testing tools such as DbUnit and enabling you to do the same with other test frameworks.

Configuring the application context

Let's take a look at services-config.xml and test-ds-config.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans>

    <bean id="phonebook" class="org.unitils.sample.phonebook.impl.HibernatePhoneBook">
        <property name="sessionFactory" ref="sessionFactoryBean"/>
    </bean>

    <bean id="sessionFactoryBean" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="annotatedClasses">
            <list>
                <value>org.unitils.sample.phonebook.domain.Person</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <value>
                hibernate.dialect=org.hibernate.dialect.HSQLDialect
                hibernate.show_sql=true
            </value>
        </property>
    </bean>
    
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <bean id="dataSource" class="org.unitils.database.UnitilsDataSource" />
</beans>

The first one, services-config.xml, is a general spring configuration file for our application, the bean definitions in this file don't need to be modified for testing purposes. The second file, test-ds-config.xml, defines the test datasource for the phonebook service. This file exists for testing purposes only, another spring configuration file exists that defines the DataSource for normal application use.

The data source is an instance of UnitilsDataSource. It is a connection pooled data source, providing access to the unit-test database. The configuration of this data source is specified in a Unitils configuration file. We won't explain how to configure Unitils in this article, for this we refer to our cookbook and tutorial pages.

Loading a test data set

Now let's return to the test. To make sure test data is loaded, we can use the @DataSet annotation either on class level (load test data for every method) or on method level (load test data for this method only). In our example, the method is annotated:

@DataSet
public void testSearch() {

This will make sure that before running the test, our test database is populated with data from a dataset file. The system searches for a DbUnit file either named UnitilsHibernatePhonebookTest.testSearch.xml or UnitilsHibernatePhonebookTest.xml. The former can be used if you want to create a dataset for one method specifically, the latter is the default one for all the tests in this test class. The dataset is formatted as a DbUnit FlatXmlDataSet as follows:

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <PERSON FIRSTNAME="John" LASTNAME="Doe" />
    <PERSON FIRSTNAME="Jane" LASTNAME="Doe" />
    <PERSON FIRSTNAME="Jim"  LASTNAME="Smith" />
</dataset>
Checking the result

After this, the unit test is executed. To express assert statements, we use Unitils' reflection assert utility. This utility uses java reflection to compare all fields of the expected and actual object and all objects that are referenced by the expected or actual ones. You can also check whether a certain property of every object in a collection matches a collection of expected values. E.g. you can specify the following:

ReflectionAssert.assertReflectionEquals(new Person("John", "Doe"), result.get(0));
ReflectionAssert.assertPropertyReflectionEquals("firstName", Arrays.asList("John", "Jane"), result);

The first assertion checks whether all field values of the Person object, in this case the first and last name, match those of the expected object. The second one checks whether the first names of the objects in the result list are "John" and "Jane".

A number of leniency levels is offered too. You can for example ignore fields that have a null/0 Java default value in the expected object or ignore the order of elements in a collection. These leniency levels are offered by default by the assertLenEquals (instead of assertRefEquals) methods. For example:

ReflectionAssert.assertLenientEquals(new Person("John", null), result.get(0));
ReflectionAssert.assertPropertyLenientEquals("firstName", Arrays.asList("John", "Jane"), result);    

The first statement verifies that the firstName field in the result object equals to "John", it ignores the other fields. The second one checks whether the values of the firstName field in the result person list are "John" and "Jane", ignoring the actual order of these values.


Hibernate mapping test

Unitils contains a very simple but powerful mapping test. This test checks whether the mapping of all Hibernate mapped objects is consistent with the database. To activate it, just add following test to your test suite:

@SpringApplicationContext({"classpath:services-config.xml", "classpath:test-ds-config.xml"})
public class HibernateMappingTest extends UnitilsJUnit3 {

    public void testMappingToDatabase() {
        HibernateUnitils.assertMappingWithDatabaseConsistent();
    }
}

Suppose that the column PERSON.FIRSTNAME is missing in the test database. This will make the test fail with a message indicating that this column should be added:

AssertionFailedError: Found mismatches between Java objects and database tables. 
Applying following DDL statements to the database should resolve the problem: 
alter table PERSON add column firstName varchar(255);

You may have noticed that the @SpringApplicationContext annotation reappears in the test. To avoid this duplication, you can create a common superclass for all database tests that contain the configuration.


Unit test database maintenance

To keep database tests maintainable, test data files have to be as small as possible. (Referential) constraints however don't help you to achieve this. We advise to disable all foreign key and not null constraints. Doing so makes it possible to specify only the data that matters for your test. To make sure tests can be run without being affected by others, we also advise to let every developer have its own test database or test database schema.

To help you with this, Unitils provides a powerful database maintainer offering features such as automatic maintainance of your unit test schemas, disabling of the foreign key and not-null constraints and generating a DTD for DbUnit dataset files. Explaining the full workings of the database maintainer would lead us too far for this article. We'll just give a simple example to give you an impression of how this works. Consult the tutorial for a more in depth example.

The database maintainer monitors a directory on the filesystem that contains DDL scripts for creating the structure of the database. The name of these scripts should comply with following naming convention: <version>_<some name>.sql. For example:

dbscripts/ 001_create_person_table.sql
           002_create_car_table.sql

Each unit test database or schema contains a table in which it stores its current version number. In this case, the version number of the database will be 2.

Suppose we add a file named 003_add_phonenumber.sql to this directory. The next time a database test is executed, the database maintainer will notice that the structure of the unit test database is no longer up to date and executes the new script on the database. It then increases the version of the database. Changes to existing files, e.g. 001_create_person_table.sql will also be detected by the database maintainer. In that case, it will drop all tables in the test database and recreate it from scratch, starting again from the first script.


Conclusion

In this article we've tried to demonstrate how Unitils can improve your tests, using a few simple examples. If you're curious to learn more about Unitils, check out the tutorial and the cookbook.

We value your opinion. Let us know what you think. If you have any comments, tips or questions please post them on the user forum or contact us directly by sending a mail to filip_neven at users.sourceforge.net or tim_ducheyne at users.sourceforge.net.


About the authors

Filip Neven and Tim Ducheyne are senior software enigneers at Ordina Belgium. They each have more than 6 years of experience in designing and developing Java enterprise applications. They were the leaders of an expert group on unit testing within Ordina. Later, they founded the Unitils open source project in which they worked out a number of their ideas. Today they are still actively working on Unitils as the project's lead developers. Recently, Filip gave a Quicky presentation on Unitils at the Javapolis conference.