Testing JPA Hibernate with Spring & DbUnit

February 4, 2009 - zenoconsultingzenoconsulting


Introduction

I've seen a lot of different approaches to building persistence layers and a lot of different approaches to testing them. I've also built a lot of them using different approaches. Some of the approaches worked ok — some didn't. Here I'm going to show you a method I really like that I have settled on as good. It has a lot of goodness. If you're starting a greenfield persistence layer with Java, take a close look and feel free to modify to suit your needs or comment.

You can download the complete source example for this here and do anything you want with it.

Assumptions

  • I'm assuming you've already made the decision to use ORM. If you haven't taken that leap, you can still build something similar, but I'd highly recommend at least using Spring's JdbcTemplate.
  • This example uses Hibernate but you can easily modify it for TopLink or any other ORM framework that supports JPA.
  • This example uses JPA but you can easily modify it to directly use your ORM's API instead. I think the benefits of JPA outweigh tying your code directly to the specific ORM implementation. The API is simple and standardized. The only reason I see for not using it is that frameworks like Hibernate offer more support for advanced features — but if you really need that, you can dig out the Hibernate specific classes in your implementation. This is cleaner than tying most of your code directly to the ORM API.
  • This example uses maven to setup and build the project. If you're new to maven I'll show you how to install and use it below. It is simple to use and saves you a lot of headache.
  • This example uses HSQLDB as the database. This is an in-memory Java relational database. The reason for this is so you don't have to set up a database. HSQL-DB is really nice for testing like this, but using Spring (as you'll see below), you can quickly tweak it to use any database you want or multiple databases.

Setup The Project

First, download the code and unzip it somewhere.

Install Maven

Download maven and unzip it somewhere. Add the following environment variables:

$ set M2_HOME=/path/to/maven
$ set M2=$M2_HOME/bin

Test it out:

$ mvn -version
Maven version: 2.0.9
Java version: 1.6.0_07
OS name: "mac os x" version: "10.5.6" arch: "x86_64" Family: "mac"

If you use Eclipse — you're in luck..follow the steps below. If you use something else, just modify the mvn commands below for whatever IDE you use (e.g. mvn intellij:intellij or mvn netbeans:netbeans)

Change to the directory where you unzipped the source and run (for Eclipse):

NOTE: the first time you do this, maven will download all the jars it needs. This may take a while. The second time you do it will go much faster.

$ mvn eclipse:eclipse
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'eclipse'.
[INFO] ------------------------------------------------------------------------
[INFO] Building persistence
[INFO]    task-segment: [eclipse:eclipse]
[INFO] ------------------------------------------------------------------------

etc.

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1 minute 40 seconds
[INFO] Finished at: Wed Feb 04 20:22:19 EST 2009
[INFO] Final Memory: 13M/32M
[INFO] ------------------------------------------------------------------------

Open Eclipse, do Import -> Existing Project From Source…

Navigate to the directory where the pom.xml file is, and voila. You'll get compiler errors, but to fix it you just have to add a simple variable in Eclipse to tell it where to find maven jars. Do this:

  1. Right-click the project -> Properties
  2. Navigate to Java Build Path
  3. Navigate to the Libraries Tab
  4. Click the Add Variable… button
  5. Click the Configure Variables… button
  6. Click the New… button
  7. The name should be M2_REPO and the value should be the path to the local maven repository:

On Windows: C:\Documents and Settings\<yourname>\.m2\repository
On Linux: /home/<yourname>/.m2/repository
On Mac: /Users/<yourname>/.m2/repository

After you do that, all compiler errors should go away.

Run the tests in Eclipse

Right click src/test/java and choose Run as JUnit test.

Run the tests via Maven

$ mvn test
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building persistence
[INFO]    task-segment: [test]
[INFO] ------------------------------------------------------------------------

etc.

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7 seconds
[INFO] Finished at: Wed Feb 04 21:09:58 EST 2009
[INFO] Final Memory: 14M/34M
[INFO] ------------------------------------------------------------------------

Hmm….7 seconds to run 4 database CRUD tests. That's not too bad, but most of that time is spent loading the spring context. If you load this once in all your CRUD tests that time is amortized, and all your CRUD tests can run fast. Nice.

Automatically generate the DDL to a file

Maven to the rescue:

$ mvn hibernate3:hbm2ddl
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'hibernate3'.
[INFO] ------------------------------------------------------------------------
[INFO] Building persistence
[INFO]    task-segment: [hibernate3:hbm2ddl]
[INFO] ------------------------------------------------------------------------

etc...

21:20:53,739 INFO  [main] hbm2ddl.SchemaExport (SchemaExport.java:174) - writing generated schema to file: /Users/davisford/Desktop/jpa-hibernate-spring-dbunit/target/hibernate3/sql/myschema.ddl

    create table MY_ENTITY (
        ID bigint generated by default as identity (start with 1),
        NAME varchar(255),
        primary key (ID)
    );
21:20:53,740 INFO  [main] hbm2ddl.SchemaExport (SchemaExport.java:196) - schema export complete
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3 seconds
[INFO] Finished at: Wed Feb 04 21:20:53 EST 2009
[INFO] Final Memory: 11M/30M
[INFO] ------------------------------------------------------------------------

Cool..you'll find the DDL under the directory target/hibernate3/sql/myschema.ddl. If you tweak the object model / JPA annotations, you can automatically re-generate the DDL to produce migration scripts for your production database.

How It All Works

The Entity Class

First, we start with an Entity class (i.e. a class that gets persisted). It is really dumb and simple:

package com.namespace;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
 
@Entity
@Table(name="MY_ENTITY")
public class MyEntity {
 
    private String name;
    private long id;
 
    MyEntity() {}
 
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    @Column(name="ID")
    public long getId() {
        return id;
    }
 
    public void setId(long id) {
        this.id = id;
    }
 
    @Column(name="NAME")
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}

The JPA @Entity annotation means that this is..well, an Entity class. @Table tells it what the table name is it maps to. @Id defines the primary key and specifies an automatic sequence generator. Those three annotations are the bare minimum you need. @Column isn't necessary — a column will get generated anyway, but if you want to control its name, you can do that like the example. This is a simple example with the focus being how to build a DAO layer and test it — not how to design your entity / relational model. You may have many entity classes with complex relationships, but I won't go into that here. That's your design.

Some people make Entity classes implement interfaces. I have not really found a good reason for this. Unless your object model is fairly complex — I don't really find a good case for making all Entity classes implement an interface just for the sake of it. Traditionally these classes just hold data with getters and setters, so there isn't a big need for subclassing / overriding blah blah.

You will very likely want the entity class to implement equals and hashCode also. The Hibernate book has a very good discussion on how best to do this. Implementing these methods does not follow the standard pattern necessarily.

The DAO Interface

The DAO should absolutely be an interface. The reason is that lots of other code may rely on this interface especially in tests, and you want to mock it so your entire unit test suite with 3-gazillion tests aren't all going to the database. Only the tests for the DAO classes have to round-trip with a real database.

This is a simple example with only a few simple common CRUD methods. You might choose to abstract this into common DAO implementation in an abstract class…your choice, your design.

package com.namespace;
 
import java.util.List;
 
public interface MyEntityDao {
 
    MyEntity find(long id);
 
    List<MyEntity> findAll();
 
    void save(MyEntity entity);
 
    void delete(long id);
}

Pretty self-explanatory interface.

The DAO Implementation

Here's where we make use of Spring's JpaTemplate to do most of the heavy lifting for us. Really what it does is manage the JPA EntityManagerFactory, EntityManager, and take care of all the exception handling in a consistent way — plus it handles rollbacks and all that good stuff. If you need a better explanation or more convincing read this.

package com.namespace;
 
import java.util.List;
 
import org.springframework.orm.jpa.JpaTemplate;
import org.springframework.transaction.annotation.Transactional;
 
public class MyEntityDaoImpl implements MyEntityDao {
 
    private JpaTemplate jpaTemplate;
 
    /* (non-Javadoc)
     * @see com.namespace.MyEntityDao#find(java.lang.Long)
     */
    public MyEntity find(long id) {
        return (MyEntity) jpaTemplate.find(MyEntity.class, id);
    }
 
    /* (non-Javadoc)
     * @see com.namespace.MyEntityDao#findAll()
     */
    @SuppressWarnings("unchecked")
    public List<MyEntity> findAll() {
        return (List<MyEntity>) jpaTemplate.find("from MyEntity");
    }
 
    /* (non-Javadoc)
     * @see com.namespace.MyEntityDao#save(com.namespace.MyEntity)
     */
    @Transactional
    public void save(MyEntity entity) {
        jpaTemplate.persist(entity);
    }
 
    /* (non-Javadoc)
     * @see com.namespace.MyEntityDao#delete(java.lang.Long)
     */
    @Transactional
    public void delete(long id) {
        jpaTemplate.remove(find(id));
    }
 
    /**
     * Get the {@link JpaTemplate}
     * @return the {@link JpaTemplate}
     */
    public JpaTemplate getJpaTemplate() {
        return jpaTemplate;
    }
 
    /**
     * Set the {@link JpaTemplate}
     * @param jpaTemplate the {@link JpaTemplate} to set
     */
    public void setJpaTemplate(JpaTemplate jpaTemplate) {
        this.jpaTemplate = jpaTemplate;
    }
}

See, JpaTemplate pretty much does everything :) The only thing here that might need explaining is jpaTemplate.find("from MyEntity"); — the string argument is EJB-QL — essentially an object-oriented query language that JPA supports. You could have said something more verbose to achieve the same: SELECT OBJECT(m) FROM MyEntity m.

We use the @Transactional annotation from spring to indicate that the method should be wrapped in a transaction.

How Do I Test It?

This is the best thing ever. You get to use the class with the longest name from the Spring framework: AbstractTransactionalDataSourceSpringContextTests. Say it 3 times fast.

See the test class below, and then some comments will follow. I don't test every single corner case. This is meant as an example for setting up a good layer with a decent test strategy — not a lesson in how to write exhaustive / fantastic test methods.

package com.namespace;
 
import java.io.FileInputStream;
import java.sql.Connection;
import java.util.List;
 
import javax.sql.DataSource;
 
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;
 
public class MyEntityDaoImplTest extends AbstractTransactionalDataSourceSpringContextTests {
 
    /** class under test */
    private MyEntityDao dao;
 
    public MyEntityDaoImplTest() {
        super();
        ApplicationContext ctx = super.getApplicationContext();
        dao = (MyEntityDao) ctx.getBean("myEntityDao");
        assertNotNull(dao);
    }
 
    @Override
    protected String[] getConfigLocations() {
        return new String[] { "spring.xml" };
    }
 
    @Override
    protected void onSetUpInTransaction() throws Exception {
        DataSource dataSource = jdbcTemplate.getDataSource();
        Connection con = DataSourceUtils.getConnection(dataSource);
        IDatabaseConnection dbUnitCon = new DatabaseConnection(con);
        IDataSet dataSet = new FlatXmlDataSet(new FileInputStream("./src/test/resources/dbunit-test-data/MyEntityDaoImpl.xml"));
 
        try {
            DatabaseOperation.REFRESH.execute(dbUnitCon, dataSet);
        } finally {
            DataSourceUtils.releaseConnection(con, dataSource);
        }
    }
 
    @Test
    public final void testFindAll() {
        List<MyEntity> list = dao.findAll();
        assertEquals("did not get expected number of entities ", 3, list.size());
    }
 
    @Test
    public final void testFindById() {
        MyEntity entity = dao.find(1L);
        assertNotNull("did not find expected entity", entity);
        entity = dao.find(3L);
        assertNotNull("did not find expected entity", entity);
        entity = dao.find(4L);
        assertNull("found entity but did not expect it", entity);
    }
 
    @Test
    public final void testSave() {
        MyEntity entity = new MyEntity();
        entity.setName("name5");        
        dao.save(entity);
 
        MyEntity actual = dao.find(4L);
        assertNotNull("expected save to work", actual);
    }
 
    @Test
    public final void testDelete() {
        MyEntity entity = dao.find(1L);
        dao.delete(entity.getId());
        MyEntity actual = dao.find(1L);
        assertNull("delete did not work", actual);
    }
}

Normally I don't use spring in unit tests, but this is technically an integration test (since it hits the DB). For unit tests, I use the setter methods to inject my own hand-generated mocks or use a mock framework. This class needs a spring context though, and I'm perfectly happy to give it because it saves me a lot of headache. You could easily put the setup stuff in your own abstract class and have all your DAO tests subclass that to ensure they all share a spring context that gets loaded once.

So, for this test I use DbUnit. You'll see it loads an XML file that contains the table data. That file sets up three rows of MY_ENTITY:

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <MY_ENTITY ID='1' NAME="name1"/>
    <MY_ENTITY ID='2' NAME="name2"/>
    <MY_ENTITY ID='3' NAME="name3"/>
</dataset>

Note that if you don't specify a column in the XML (e.g. if you left out NAME on one row), DbUnit assumes you want to insert NULL.

So the test class constructor loads the spring application context from spring.xml. You override the getConfigLocations() method to tell spring what XML files to load. The magic happens in onSetUpInTransaction() — this is where we use DbUnit to refresh the database before every test method runs. Now, I can query for, delete or update any of the rows in MY_ENTITY with ids 1, 2, 3.

The really cool thing is when you use AbstractTransactionalDataSourceSpringContextTests it doesn't actually do a commit. It rolls back after the test method finishes. If you really do want to commit, you can (see the javadoc). This actually makes it really useful if you have to test against an already existing database with data — where you don't want to actually modify it. You can test against it, but it gets all rolled back after the test completes leaving no side-effects.

Plus — each test method has no database side effects. The best thing of all — removing the spring context loading — all of it is really fast. No more waiting hours to finish database tests. All the rest of the test cases for classes that depend on MyEntityDao should be injecting mock implementations of this class.

What's The Configuration Look Like?

Ok, that was the code, here's all the XML nastiness.

persistence.xml

JPA requires this to exist under META-INF. If you look at all the JPA tutorials on the web, most of them specify a bunch of stuff in here. This is not very flexible. You can only have one persistence.xml file so if you define all your database properties / urls / etc. in there, you can't easily change it. Yuck. Now look at my persistence.xml:

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
                          http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
   version="1.0">
   <persistence-unit name="myPersistenceUnit" transaction-type="RESOURCE_LOCAL">
      <!-- all this is configured via spring -->
   </persistence-unit>
</persistence>

The only thing I define here is the persistence unit name.

spring.xml

Here's the configuration for spring; comments follow

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:amq="http://activemq.apache.org/schema/core" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
                        http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
                        http://www.springframework.org/schema/tx
                          http://www.springframework.org/schema/tx/spring-tx.xsd">
 
    <description>
    Spring config
    </description>
 
    <!-- HSQL-DB memory database; for testing only -->
    <bean id="hsqlMemoryDb" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="org.hsqldb.jdbcDriver" />
        <property name="url" value="jdbc:hsqldb:mem:mypersistence" />
        <property name="username" value="sa" />
        <property name="password" value="" />
    </bean>
 
    <!-- LOCAL entity manager factory -->
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="persistenceUnitName" value="myPersistenceUnit"/>
        <property name="dataSource" ref="hsqlMemoryDb" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="showSql" value="true" />
                <property name="generateDdl" value="true" />
                <property name="databasePlatform" value="org.hibernate.dialect.HSQLDialect" />
            </bean>
        </property>
    </bean>
 
    <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>
 
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
        <property name="dataSource" ref="hsqlMemoryDb" />
    </bean>
 
    <tx:annotation-driven transaction-manager="transactionManager"/>
 
    <bean id="jpaTemplate" class="org.springframework.orm.jpa.JpaTemplate">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>
 
    <!-- DAOs -->
    <bean id="myEntityDao" class="com.namespace.MyEntityDaoImpl">
        <property name="jpaTemplate" ref="jpaTemplate"/>
    </bean>
 
</beans>

Some comments on each bean:

  • hsqlMemoryDb this defines the database we'll use. You can set up more of these for Oracle, MySql, whatever. Normally I would not put the JDBC URL, username and password in here. I'd put it in a properties file, and use Spring's PropertyPlaceholderConfigurer to inject the values dynamically from the properties file.
  • entityManagerFactory here's where you tell spring what JPA implementation to use (Hibernate) and what database to use (HSQL-DB) and you can configure the ORM properties here (e.g. whether to pretty pring SQL on the console, etc.). You'd usually see this stuff in persistence.xml if you read someone else's horrible blog.
  • PersistenceAnnotationBeanPostProcessor adds support for the spring @Transactional annotation I described earlier
  • transactionManager defines a transaction manager to do the..umm…transactions
  • <tx:annotation-driven transaction-manager="transactionManager"/> says we want to do annotation driven transactions, and specifies what transaction manager to use
  • jpaTemplate specifies the JpaTemplate and tells it what EntityManager to use
  • myEntityDao ah…here's my entity DAO, and we inject the JpaTemplate

pom.xml

Here's the maven configuration. The beauty about maven is that I define all the jar dependencies here, and when you run maven it just auto-magically downloads them for you. Running mvn eclipse:eclipse auto-magically creates eclipse .project and .classpath files and sets up the classpath to just work for you in your IDE. No headaches.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.namespace</groupId>
    <artifactId>persistence</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>persistence</name>
    <url>http://maven.apache.org</url>
    <dependencies>
        <dependency>
            <!-- spring -->
            <groupId>org.springframework</groupId>
            <artifactId>spring</artifactId>
            <version>2.5.5</version>
        </dependency>
        <dependency>
            <!-- needed by spring -->
            <groupId>org.apache.xbean</groupId>
            <artifactId>xbean-spring</artifactId>
            <version>3.4.3</version>
        </dependency>
        <dependency>
            <!-- for logging -->
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.13</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <!-- hibernate JPA support -->
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-annotations</artifactId>
            <version>3.4.0.GA</version>
        </dependency>
        <dependency>
            <!-- hibernate entity manager -->
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>3.4.0.GA</version>
        </dependency>
        <dependency>
            <!-- runtime requirement hibernate needs -->
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.5.2</version>
        </dependency>
        <!-- TEST SCOPE DEPENDENCIES -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.4</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <!-- HSQL-DB for in-memory database testing -->
            <groupId>hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <version>1.8.0.7</version>
        </dependency>
        <dependency>
            <!-- spring test; useful for DB testing -->
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>2.5.5</version>
        </dependency>
        <dependency>
            <!-- DbUnit for DB testing -->
            <groupId>org.dbunit</groupId>
            <artifactId>dbunit</artifactId>
            <version>2.4.2</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <!-- HIBERNATE 3 PLUGIN - this can output the DDL to a file -->
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>hibernate3-maven-plugin</artifactId>
                <version>2.0-alpha-2</version>
                <executions>
                    <execution>
                        <phase>process-classes</phase>
                        <goals>
                            <goal>hbm2ddl</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <components>
                        <component>
                            <name>hbm2ddl</name>
                            <implementation>annotationconfiguration</implementation>
                        </component>
                    </components>
                    <componentProperties>
                        <implementation>jpaconfiguration</implementation>
                        <persistenceunit>myPersistenceUnit</persistenceunit>
                        <outputfilename>myschema.ddl</outputfilename>
                        <propertyfile>${basedir}/src/main/resources/database.properties</propertyfile>
                        <drop>false</drop>
                        <create>true</create>
                        <export>false</export>
                        <format>true</format>
                    </componentProperties>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-eclipse-plugin</artifactId>
                <configuration>
                    <downloadSources>true</downloadSources>
                    <downloadJavadocs>true</downloadJavadocs>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.5</source>
                    <target>1.5</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

None of the stuff in the <plugins> tag is strictly necessary. It is just cake-icing. maven-compiler-plugin can force maven to adhere to a specific java version. I tell maven-eclipse-plugin to also download the source jars and the javadocs if they are available — very useful inside the IDE. The hibernate3-maven-plugin I already demonstrated above — it can auto-generate the DDL for you.


Backlinks


Add a New Comment
or Sign in as Wikidot user
(will not be published)
- +
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License