Unit Testing Hibernate Mapping Configurations Unit Testing Hibernate Mapping Configurations

by Johannes Brodwall
10/11/2005

Contents
Getting Started
Step 1: Write a Test
Step 2: Make the Test Compile
Step 3: The DAO
Step 4: Test Harness for the HibernateTemplate
Step 5: Hibernate Mapping
Step 6: Fixing the Error in the Mapping File
Summary: The Test Harness
Step 7: Products Contain Components
Step 8: Extending the Hibernate Mapping Test
Step 9: Creating the One-to-Many Association
Step 10: Eager Fetching
Step 11: Lazy Loading and Sessions
Step 12: The Session Must Be Flushed
Summary
Resources

In the last few years, Hibernate has become one of the most popular Java open source frameworks available. However, developers don't always remember that the mapping files that drive Hibernate's behavior are as much a part of the program as the Java code. These files can contain defects, behave unexpectedly, and break when you change other parts of your system. In this article, I will show how you can use unit testing to assess the correctness of your Hibernate configuration. The article is a step-by-step approach that also explains some of the more common difficulties you may encounter while using Hibernate.

Getting Started

To get started, you'll need a copy of each of the following. Everything is open source and can be found by accessing the sites listed in the Resources section:

You need to set up a project to use Hibernate, JUnit, Hypersonic, and Spring. You have to know how to set up the classpath in your environment, but I will tell you which .jar files you should add along the way. You do not need any prior knowledge of these tools to follow this article. The companion source archive (see "Resources") includes all of the required .jars and an Eclipse project definition that sets up the project correctly.

This tutorial assumes that you already have a Product class, which has its own tests. In particular, make sure that you test the behavior of equals and hashCode, as Hibernate relies on these to work correctly. I recommend using Mike Bowler's GSBase library for testing this (see "Resources").

We want to persist the following class to the database using Hibernate (in compressed pseudo-code).

class no.brodwall.demo.domain.Product {
    String productName;
    Long id;
}

Step 1: Write a Test

In test-driven development, you should always start the development cycle by writing a test. To do this, you must first add junit.jar to your project's class path. We will begin by writing a simple test that defines some of the behavior of our DAO: we simply create a Product, save it, and ensure that when we try to load it again, we get the same Product:

package no.brodwall.demo.domain.persist;

import no.brodwall.demo.domain.Product;
import junit.framework.TestCase;

public class ProductDAOTest extends TestCase {

    private ProductDAO productDAO = new ProductDAO();

    public void testStoreRetrieve() {
        Product product = new Product("My product");
        long id = productDAO.store(product);

        Product retrievedProduct = productDAO.get(id);
        assertEquals(product, retrievedProduct);
        assertNotSame(product, retrievedProduct);
    }

}

Step 2: Make the Test Compile

This code fails to compile because I don't have ProductDAO. Let's create it:

package no.brodwall.demo.domain.persist;

import no.brodwall.demo.domain.Product;

public class ProductDAO {

    public long store(Product product) {
        // TODO Auto-generated method stub
        return 0;
    }

    public Product get(long id) {
        // TODO Auto-generated method stub
        return null;
    }

}

Run the test. It fails because ProductDAO.get returns null.

Step 3: The DAO

To simplify the DAO implementation, we want to use Spring's HibernateTemplate and inject it into the DAO. This is part of Spring's ORM support. To include this functionality, you must add these files to your classpath: spring.jar, commons-logging.jar, and jta.jar. All of these files are available as parts of the Spring Framework download. You should also add hibernate.jar from the Hibernate download. We use HibernateTemplate in PersonDAOTest.setUp:


import org.springframework.orm.hibernate3.HibernateTemplate;

...

    protected void setUp() throws Exception {
        HibernateTemplate hibernateTemplate =
            new HibernateTemplate();
        productDAO.setHibernateTemplate(hibernateTemplate);
    }

Add a setter for HibernateTemplate to the DAO, and it compiles. Of course, we still get the same error. To fix things, we have to implement the ProductDAO:

package no.brodwall.demo.domain.persist;

import org.springframework.orm.hibernate3.HibernateTemplate;
import no.brodwall.demo.domain.Product;

public class ProductDAO {

    private HibernateTemplate hibernateTemplate;

    public long store(Product product) {
        Long key = 
            (Long)hibernateTemplate.save(product);
        return key.longValue();
    }

    public Product get(long id) {
        return (Product)hibernateTemplate.get(
             Product.class, new Long(id));
    }

    public void setHibernateTemplate (
        HibernateTemplate hibernateTemplate) {
        this.hibernateTemplate = hibernateTemplate;
    }

}

As you can see, there is not much here beyond the use of HibernateTemplate. To clean up the code further, I also suggest inheriting from HibernateDaoSupport. You can make this change if you want to.

Running the test still produces an error, but this time it's different: java.lang.IllegalArgumentException: No SessionFactory specified. From the stack trace, it's obvious that we need to set the SessionFactory on the HibernateTemplate.

Step 4: Test Harness for the HibernateTemplate

Now comes the magic part. We're going to create a SessionFactory in the test to give to the HibernateTemplate in the DAO. SessionFactory is part of Hibernate, so we have to add the core Hibernate .jar files (in addition to hibernate.jar, which Spring required) to the classpath: ehcache.jar, asm.jar, cglib.jar, common-collections.jar, and dom4j.jar. Since we're using HSQLDB as our database, we also have to add hsqldb.jar to the classpath. This is a lot of .jars, but hsqldb.jar is the last one we'll need.

The SessionFactory has to refer to the HSQLDB in-memory connection. After a lot of experimenting, I have found that the following code is the easiest:


import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.HSQLDialect;

....

    protected void setUp() throws Exception {
        Configuration configuration = 
            new Configuration();
        configuration.setProperty(
            Environment.DRIVER, 
            "org.hsqldb.jdbcDriver");
        configuration.setProperty(
            Environment.URL, 
            "jdbc:hsqldb:mem:ProductDAOTest");
        configuration.setProperty(
            Environment.USER, "sa");
        configuration.setProperty(
            Environment.DIALECT, 
            HSQLDialect.class.getName());
        configuration.setProperty(
            Environment.SHOW_SQL, "true");

        SessionFactory sessionFactory = 
            configuration.buildSessionFactory();
        
        HibernateTemplate hibernateTemplate = 
            new HibernateTemplate(sessionFactory);
        productDAO.setHibernateTemplate(
            hibernateTemplate);
    }

The SHOW_SQL property is not required, but it will help you see what Hibernate is doing, so I like to leave it on.

Now the test takes longer, as Hibernate is initializing. We get another error: org.springframework.orm.hibernate3.HibernateSystemException: Unknown entity: no.brodwall.demo.domain.Product. This is clear enough. We forgot to tell Hibernate about our Product class. This fix is easy:

protected void setUp() throws Exception {
    // ...
    configuration.setProperty(Environment.SHOW_SQL, "true");
    configuration.addClass(Product.class);
    SessionFactory sessionFactory = configuration.buildSessionFactory();

Now we get where we want to be: org.hibernate.MappingException: Resource: no/brodwall/demo/domain/Product.hbm.xml not found. The initial steps are over, and our test is now guiding our next step: to create the mapping file.

Step 5: Hibernate Mapping

Here is a first crack at the Hibernate mapping file for the Product class:


<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping auto-import="true" 
        package="no.brodwall.demo.domain">
    <class name="Product">
        <id name="id">
            <generator class="native" />
        </id>
    </class>
</hibernate-mapping>

As far as Hibernate mappings go, this is just about as simple as it can get: one class with only a primary key.

A new error shows the way: org.springframework.jdbc.BadSqlGrammarException: Hibernate operation: could not insert: [no.brodwall.demo.domain.Product]; bad SQL grammar [insert into Product (id) values (null)]; nested exception is java.sql.SQLException: Table not found: PRODUCT in statement [insert into Product (id) values (null)].

Hibernate's error message can be a little intimidating at first, but if you take a few breaths and read the message again, it's pretty informative. Hibernate is trying to tell you that you need to create a PRODUCT table.

We want to extend our test harness to automatically generate the database table. This is surprisingly simple:


protected void setUp() throws Exception {
    ...
        configuration.setProperty(
            Environment.SHOW_SQL, "true");
        configuration.setProperty(
            Environment.HBM2DDL_AUTO, "create-drop");
        configuration.addClass(Product.class);

The HBM2DDL_AUTO property makes Hibernate create the database schema when it first connects, and drop it before releasing a connection. JUnit calls setUp before each test method (any method starting with "testXXX"). By recreating the schema for each test method, JUnit ensures that the database is squeaky clean before each test. This is important: by setting up a fresh database for every test, we achieve proper isolation; otherwise, data inserted by one test could influence another test. When using an in-memory database like HSQLDB, this setup doesn't take an unreasonably long time.

Step 6: Fixing the Error in the Mapping File

The mapping file does not map the productName property, which leads to this final error: junit.framework.AssertionFailedError: expected: <no.brodwall.demo.domain.Product@1b0bdc8[id=1,productName=My product]> but was:<no.brodwall.demo.domain.Product@1d95da8[id=1,productName=<null>]> (if you give Product a nice toString method, that is). If you didn't get this error, you probably didn't implement the equals method correctly.

The cause for the error is quite obvious: The productName for the retrieved product is null. To fix it, we correct the Hibernate mapping for Product:


<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping auto-import="true"
        package="no.brodwall.demo.domain">
    <class name="Product">
        <id name="id">
            <generator class="native" />
        </id>
        <property name="productName"/>
    </class>
</hibernate-mapping>

And the test passes!

Summary: The Test Harness

The final bit of code to set up the Hibernate SessionFactory in the test, like this:


protected void setUp() throws Exception {
    Configuration configuration = 
        new Configuration();
    configuration.setProperty(
        Environment.DRIVER, 
        "org.hsqldb.jdbcDriver");
    configuration.setProperty(
        Environment.URL, 
        "jdbc:hsqldb:mem:ProductDAOTest");
    configuration.setProperty(
        Environment.USER, "sa");
    configuration.setProperty(
        Environment.DIALECT, 
        HSQLDialect.class.getName());
    configuration.setProperty(
        Environment.SHOW_SQL, "true");
    configuration.setProperty(
        Environment.HBM2DDL_AUTO, "create-drop");
    configuration.addClass(Product.class);

    SessionFactory sessionFactory = 
        configuration.buildSessionFactory();

    HibernateTemplate hibernateTemplate =
        new HibernateTemplate(sessionFactory);
    productDAO.setHibernateTemplate(hibernateTemplate);
}

This code can be refactored into a common superclass to effectively test Hibernate DAOs. This particular test runs as a standalone in the IDE, with no other dependencies than the necessary .jar files for Hibernate, Spring, and Hypersonic. On my computer, this test takes about three seconds.

We have now used a test-driven approach to make sure that our Hibernate mapping was correct. This may seem like overkill, given that the mapping was very simple, but the principles we have learned here will enable us to test thornier mappings. We will continue with a more advanced example, but I suggest you take a little break first. Maybe get some coffee. We've been through a lot together, and I don't know about you, but my head is a little full now. See you back in five!

Step 7: Products Contain Components

Welcome back! I hope you feel refreshed. In the previous section, we tested the Hibernate configuration for a single class. Now we will test an area of Hibernate that takes many developers a long time to become comfortable with: associations.

The basic association is that of a parent-child relationship. For example, let's say that a product has several "components." Here is the component in pseudo-code:


class no.brodwall.demo.domain.Component {
    Product product;
    String componentName;
    Long id;
}

Now you need to modify Product, so that a product has a Set of Components associated with it. Just add getters and setters for a components set property.

Again, be sure to create a separate unit test for all functionality in the Component class. Pay special attention to hashCode and equals. The first time you do this, you probably will end up with a mutually recursive call between Component.equals and Product.equals. You don't want to do that.

Here is an example of a test of the domain objects:


public void testProductComponents() {
    Product p1 = new Product("p1");
    Component c1 = new Component("c1");
    Component c2 = new Component("c2");
    p1.addComponent(c1);
    assertEquals("c1.parent", p1, c1.getProduct());
    assertNull("c2.parent", c2.getProduct());
    p1.addComponent(c2);
    assertEquals("c2.parent", p1, c2.getProduct());
    assertEquals("p1.components.size", 2, p1.getComponents().size());
}

Step 8: Extending the Hibernate Mapping Test

Now we know that the classes work, and we're ready to try out the Hibernate mapping. But before we write it, let's test it. Add the following test method to your ProductDAOTest:


public void testComponent() {
    Product product = new Product("My product");
    product.addComponent(new Component("c1"));
    product.addComponent(new Component("c2"));
    long id = productDAO.store(product);

    Product retrivedProduct = productDAO.get(id);
    assertEquals("retrievedProduct.components.size", 2, 
            retrievedProduct.getComponents().size());
}

In time, we'll probably want to expand on this, but it's good for now. Here's the error: junit.framework.AssertionFailedError: retrivedProduct.components.size expected:<2> but was:<0>.

Pretty mysterious at first, but the answer is quite simple: if a property is not listed in the Hibernate mapping, it's simply omitted while writing to the database. So we have to include the component set in the Product.hbm.xml mapping file. This is what the file currently looks like:


<!DOCTYPE hibernate-mapping PUBLIC
  "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
  "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping auto-import="true"
        package="no.brodwall.demo.domain">
     <class name="Product">
        <id name="id">
            <generator class="native" />
        </id>
        <property name="productName"/>
    </class>
</hibernate-mapping>

We can't just use <property> for the components property. If we did, Hibernate would choke on the mapping file with the following message: org.hibernate.MappingException: Could not determine type for: java.util.Set, for columns: [org.hibernate.mapping.Column(components)]. Fair enough.

Step 9: Creating the One-to-Many Association

The Hibernate reference manual is excellent when it comes to describing mapping relationships (see Chapter 8, " Association Mappings"). What we want is a bidirectional one-to-many association (Chapter 8.4.1). We need to make two mapping files, then.

Here is the first, /no/brodwall/demo/domain/Component.hbm.xml:


<!DOCTYPE hibernate-mapping PUBLIC
  "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
  "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping auto-import="true"
        package="no.brodwall.demo.domain">
    <class name="Component">
        <id name="id">
            <generator class="native" />
        </id>
        <property name="componentName"/>
        <many-to-one name="parent" 
                        column="parentId"
                        not-null="true" />
    </class>
</hibernate-mapping>

And here is the second, /no/brodwall/demo/domain/Product.hbm.xml:


<!DOCTYPE hibernate-mapping PUBLIC
  "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
  "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping auto-import="true"
        package="no.brodwall.demo.domain">
    <class name="Product">
        <id name="id">
            <generator class="native" />
        </id>
        <property name="productName"/>
        <set name="components" cascade="all">
            <key column="productId"/>
            <one-to-many class="Component"/>
        </set>
    </class>

</hibernate-mapping>

This is taken directly from the Hibernate reference documentation; Chapter 8.4.1.

So far, so good. We also have to make sure to include the Component class in the list of classes known to Hibernate in ProductDAOTest.setUp:


protected void setUp() throws Exception {
    ...
    configuration.addClass(Product.class);
    configuration.addClass(Component.class);

Now run it again, and testComponent fails with the following: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: no.brodwall.demo.domain.Product.components - no session or session was closed.

Step 10: Eager Fetching

This was an error I struggled with for a long time when I tried to migrate my code to Hibernate 3. As it turns out, the default behavior for Hibernate 2.1 was to eagerly load associations. The default with Hibernate 3 is to load them lazily. Lazy loading is good, and we want to keep it, but just as an experiment, let's try loading Component eagerly. Make the following change in Component.hbm.xml:


<hibernate-mapping auto-import="true"
        package="no.brodwall.demo.domain">

    <class name="Product">
    ...
        <set name="components" lazy="false"
                cascade="all">
        ...

At this point, the tests should succeed. But this actually isn't the right answer.

Step 11: Lazy Loading and Sessions

We want the relationship between Product and Component to be lazily loaded, so remove the lazy="false" attribute on the Product.components, and let's try another way.

What we need to do is to ensure that the Hibernate session stays open for the whole time between the call to productDAO.get and the access of the collection. Here is how to do this.

Spring's HibernateTemplate uses a clever mechanism for getting the Hibernate Session ( HibernateTemplate.getSession). If a session is associated with the current thread, HibernateTemplate uses that one, otherwise save and similar methods will open a new session from the SessionFactory, execute its commands, and close it again. When Hibernate is used in servlets, a ServletFilter is set up to intercept all HTTP requests, open the session, and then close it when the request is done processing. This pattern is called "Open Session in View."

We're not running in a servlet but in a JUnit test. To make the session stay open, we have to associate a session with the current thread. We can do this by using the Spring class SessionFactoryUtils. Change ProductDAOTest.testComponent to the following:


public void testComponent() {
    Product product = new Product("My product");
    product.addComponent(new Component("c1"));
    product.addComponent(new Component("c2"));
    Long id = productDAO.store(product);

    Session session = SessionFactoryUtils.
            getSession(this.sessionFactory, true);
    TransactionSynchronizationManager.
            bindResource(this.sessionFactory, 
                    new SessionHolder(session));

    Product retrievedProduct = productDAO.get(id);
    assertEquals("retrievedProduct.components.size",
            2,
            retrievedProduct.getComponents().size());

    TransactionSynchronizationManager.
            unbindResource(sessionFactory);
    SessionFactoryUtils.
            releaseSession(session, sessionFactory);
}

Notice that you will also need to change the sessionFactory we initialize in ProductDAOTest.setUp to an instance variable instead of a local variable.

The test should now run successfully, and lazy loading is implemented. I would love to tell you what the deal is with the TransactionSynchronizationManager, but I honestly don't know. It has to be present, though, or we will still get LazyInitializationException.

Now it's time for a refactoring: opening and closing the session is something that we want to do for all test methods, so it's a good candidate for moving into setUp and tearDown. After refactoring, the methods look like this:


protected void setUp() throws Exception {
    Configuration configuration = 
        new Configuration();
    configuration.setProperty(
        Environment.DRIVER, 
        "org.hsqldb.jdbcDriver");
    configuration.setProperty(
        Environment.URL, 
        "jdbc:hsqldb:mem:ProductDAOTest");
    configuration.setProperty(
        Environment.USER, "sa");
    configuration.setProperty(
        Environment.DIALECT, 
        HSQLDialect.class.getName());
    configuration.setProperty(
        Environment.SHOW_SQL, "true");
    configuration.setProperty(
        Environment.HBM2DDL_AUTO, "create-drop");
    configuration.addClass(Product.class);
    configuration.addClass(Component.class);

    this.sessionFactory = 
        configuration.buildSessionFactory();

    HibernateTemplate hibernateTemplate =
        new HibernateTemplate(sessionFactory);
    productDAO.setHibernateTemplate(hibernateTemplate);

    this.session = SessionFactoryUtils.
        getSession(sessionFactory, true);
    TransactionSynchronizationManager.
        bindResource(sessionFactory, 
            new SessionHolder(session));
}

protected void tearDown() throws Exception {
    TransactionSynchronizationManager.
        unbindResource(sessionFactory);
    SessionFactoryUtils.
        releaseSession(session, sessionFactory);
}

Now something curious happens: testStoreRetrieve from the first part of the article fails! This is the code:


public void testStoreRetrieve() {
    Product product = new Product("My product");
    Long id = productDAO.store(product);

    Product retrievedProduct = productDAO.get(id);
    assertEquals(product, retrievedProduct);
    assertNotSame(product, retrievedProduct);
}

It fails because Hibernate's Session will ensure that only one instance of the "My Product" Product exists per session. We store and get the Product in the same session, so get returns the object we stored, and consequently assertNotSame fails. We need to do more work.

Step 12: The Session Must Be Flushed

We saw that Hibernate ensures that two copies of the same data loaded in the same session resolve to the same object instance. This defeats the purpose of our test, as the object we save is never really retrieved from the data store. The cache of data in the session used to implement this is called Hibernate's "first-level cache" or "session cache."

To avoid getting retrievedProduct from the session cache, we have to ask Hibernate to clear it, which we can do by calling HibernateSession.clear. We have to make hibernateSession into an instance variable on the test so we can call it from the test method. Here is the final test:


public void testStoreRetrieve() {
    Product product = new Product("My product");
    Long id = productDAO.store(product);
    hibernateTemplate.flush();
    hibernateTemplate.clear();

    Product retrievedProduct = productDAO.get(id);
    assertNotSame(product, retrievedProduct);
    assertEquals(product, retrievedProduct);
}

public void testComponent() {
    Product product = new Product("My product");
    product.addComponent(new Component("c1"));
    product.addComponent(new Component("c2"));
    Long id = productDAO.store(product);
    hibernateTemplate.flush();
    hibernateTemplate.clear();

    Product retrievedProduct = productDAO.get(id);
    assertNotSame(product, retrievedProduct);
    assertEquals("retrievedProduct.components.size", 2,
            retrievedProduct.getComponents().size());
}

The test now passes, and we have a complete pattern for testing to ensure that our objects are persisted correctly.

Using this test class, you can test all kinds of contortions with Hibernate mapping. There are many challenges to getting an advanced mapping right, and having a test framework gives you a good place to start.

Summary

Using the magic of Spring's Hibernate support, we were able to test a parent-child relationship. To be able to fetch the lazy association, we had to ensure that the Session stayed open for the whole test method. To avoid using the same Session, and therefore the same objects when reading and writing, we had to be sure we flushed Hibernate's session cache between writing and reading our objects.

Unit tests for Hibernate configuration are very useful whenever the mapping or the mapped classes change. A properly written test of the Hibernate configuration will help you if you add, rename, or remove a field in the class but forget to update the Hibernate mapping file. During development, testing the configuration early can help you get the trickier bits of Hibernate, such as inheritance and relationships, right from the start, saving you lots of debugging effort down the line.

Resources

Johannes Brodwall is currently lead Java architect at BBS, the company that operates Norway's banking infrastructure


 Feed java.net RSS Feeds