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:
Hibernate. You can't do
much Hibernate testing without Hibernate.
The Hypersonic
embedded database (HSQLDB). This provides an in-memory database for
easier testing and up.
The Spring
Framework Object-Relation Mapping (ORM) components. These provide a set of classes that
make it easier to write Data Access Objects (DAOs). You don't need
to use Spring wholesale to take advantage of these features.
A good IDE. I strongly recommending using a good IDE like IntelliJ, Eclipse, or NetBeans. As a matter of fact, all
of the tests in this article can be run from the IDE JUnit
plugin.
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:
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:
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:
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:
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:
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:
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!