Saturday, November 11, 2006

Testing Hibernate DAOs with Spring

Spring has an enormous amount of utility/help/glue code, that can simplify a lot of common tasks, but it's not easy to get a good overview of everything that's available. One of the core areas of Spring is of course testing, and I'm going to show one way to write unit tests (or integration tests, if you insist) for Data Access Objects implemented using Hibernate, against a real database.

To begin with, most of the work has already been done by Spring. In the
org.springframework.test package there are a number of helper classes on top of JUnit for in-container testing. We are going to make use of an application context, a data source and transactions, so we're going to with AbstractTransactionalDataSourceSpringContextTests, which is coincidentally the longest class name in Spring. This class has a number of cool features:

  • It keeps a static cache of application contexts based on the string array of file names defining the context, so you only need to initialize the context once per test class*.
  • It autowires the test itself, by type. (It's also possible to change to autowire-by-name, or turn it off)*
  • It runs every test inside a transaction, which is rolled back after each test (unless you say otherwise).
  • It makes a JdbcTemplate available for low-level database operations.
  • Instead of the setUp()/tearDown() callbacks, it has callbacks before transactions start, inside the transaction but before the test, after the test but inside the transaction and finally after the transaction has been rolled back.
(* Thanks Robert for the pointers.)

I usually organize my applicationContext files so that every environment-dependant bean - such as the data source, SMTP server or whatever - is in a separate file called applicationContext-env.xml. Also, each of the dao-, service- and mvc layer have their own context file. Using that setup we would use the applicationContext-dao.xml file, and a test-specific applicationContext-env.xml. If you keep your context files in the classpath and use Maven and the standard directory layout, overriding context files work out of the box. Specifically, you can keep one version of applicationContext-env.xml in src/main/resources pointing at a JNDI exposed data source that's picked up from the application server, and another in src/test/resources looking somthing like this:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="jdbc:postgresql:myapp_test"/>
<property name="driverClassName" value="org.postgresql.Driver"/>
<property name="username" value="my_db_user"/>
<property name="password" value="******"/>
</bean>

In addition to this, you can have a test-specific hibernate.properties in src/test/resources that turns on SQL debugging and other things:

hibernate.show_sql=true
hibernate.format_sql=true

Don't deviate too much from the production environment though, by doing things like turning off the second-level cache or whatever, since that decreases the value of the tests. We want our testing to be as close to reality as possible, that's why where using the same database (application) as in production, instead of something like Hypersonic.

Now we're ready to implement our first method, which we'll do in a base class for all our daos. You ususally have a number of daos that use the same combination of context files, the same batch file with test data and which all need a reference to the SessionFactory. So, here's our AbstractDaoIntegrationTest class:

public abstract class AbstractDaoIntegrationTest extends AbstractTransactionalDataSourceSpringContextTests {

SessionFactory sessionFactory;
SimpleJdbcTemplate jt;

public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}

@Override
protected String[] getConfigLocations() {
return new String[] {
"applicationContext-dao.xml",
"applicationContext-env.xml"
};
}

@Override
protected void onSetUpBeforeTransaction() throws Exception {
jt = new SimpleJdbcTemplate(jdbcTemplate);
}

@Override
protected void onSetUpInTransaction() throws Exception {
super.onSetUpInTransaction();
// Load a batch of test data in the transaction
executeSqlScript("classpath:test-data.sql", false);
}

// Utility method
protected void flush() {
sessionFactory.getCurrentSession().flush();
}

}


The SessionFactory bean obviously needs to be available in one of the context files, I usually keep ORM configuration along with dao declarations. As you can see, an SQL file of test data is loaded at the beginning of the transaction. This data is rolled back at the end, eliminating the need for any manual cleanup. This works well for any reasonable amount of test data, but maybe not if you need a million records loaded. The actual test will be hard-coded against the test data file, when verifying load and finder methods. We're also wrapping the JdbcTemplate with the new and simpler (!) SimpleJdbcTemplate, which makes use of varags for parameter matching for example.

Now we can start writing dao tests on top of the above class. I'm going to use an EmployeeDao for this blog entry.

public class EmployeeDaoTest extends AbstractDaoIntegrationTest {

EmployeeDao employeeDao;

public void setEmployeeDao(EmployeeDao employeeDao) {
this.employeeDao = employeeDao;
}

}

This is all we need to get started with the actual tests! Very clean and simple. Let's look at load, store and delete testing in turn.

public void testLoadEmployee() throws Exception {
Employee employee = employeeDao.load(7L);

assertEquals("197708170000", employee.getUsername());
assertEquals("test-password", employee.getPassword());
assertEquals("test.realm", employee.getRealm());
assertEquals("197708170000@test.realm", employee.getJid());
assertEquals("test-firstName test-familyName", employee.getName());
// Assert every property
}

The important principle in action here is that you test one ORM operation against a known database state, namely the test batch file.

Note that unless we close the Hibernate session manually, the session remains open during the test, so that lazy relational restrictions will not be effective here. We'll test that later though.

Store testing is similar, we build an Employee in Java code, store it using the ORM, and the compare against the actual database state.

public void testStore() throws Exception {
Employee employee = new Employee();
employee.setUsername("197708306621");
employee.setPassword("_password");
employee.setRealm("_realm");
employee.getRoles().add(User.Role.CUSTOMER);
employee.getRoles().add(User.Role.EMPLOYEE);
employee.getRoles().add(User.Role.TRANSLATOR);
employee.getVcard().setFirstName("_firstName");
employee.getVcard().setFamilyName("_familyName");

// Store
employeeDao.store(employee);
// Flush manually, to make sure the SQL is issued
flush();

// Start assertions by loading data from the database using the JdbcTemplate
assertNotNull(employee.getId());
Map authregMap = jt.queryForMap(
"select * from authreg where id = ? " +
"and class = 'se.petrix.iaba.model.user.Employee'", employee.getId());

assertEquals("197708306621", authregMap.get("username"));
assertEquals("_password", authregMap.get("password"));
assertEquals("_realm", authregMap.get("realm"));

// Roles
List<Map<String,Role>> roles = jt.queryForList(
"select role from authreg_roles where authreg ? order by role",
employee.getId());

assertEquals(3, roles.size());
assertEquals(Role.CUSTOMER.name(), roles.get(0).get("role"));
assertEquals(Role.EMPLOYEE.name(), roles.get(1).get("role"));
assertEquals(Role.TRANSLATOR.name(), roles.get(2).get("role"));
}


Here you can see the SimpleJdbcTemplate in action. It's really simple to read data for verifying against what we fed the Employee instance with.

Next comes deletion, which is fairly simple in comparison.

public void testDelete() throws Exception {
employeeDao.delete(7L);
flush();

// Employee
assertEquals(0, jt.queryForInt("select count(*) from authreg where id = 7"));
// Roles
assertEquals(0, jt.queryForInt("select count(*) from authreg_roles where authreg_id = 7"));
}

Here's where you would test your cascade-delete mapping settings, by making sure that data in related tables is or isn't deleted, according to your configuration.

Finally we'll take a look at how you can test lazy relations. It could be argued that this belongs in the service layer tests, but the same method can easily be applied to the service layer, just include applicationContext-service.xml and write tests against that layer.

Suppose that Employee has a Set<Customer> of customers that he or she is responsible for, and that the relation is lazy. An Employee also has a Set<Role> of roles, that isn't lazy.

public void testEmployeeRelations() throws Exception {
Employee employee = employeeDao.load(7L);
// Manually end the transaction, which closes the Hibernate session
endTransaction();

try {
Role role = employee.getRoles().iterator.next();
// Success
} catch (LazyInitializationException e) {
fail("Employee.roles relation should not be lazy");
}

try {
Customer customer = employee.getCustomers().iterator.next();
fail("Employee.customers relation should be lazy");
} catch (LazyInitializationException e) {
// Success
}
}

That's it for today, good luck and as always, comments are welcome.

2 comments:

Chris Worley said...

How can the developer have a session live during the during the life of the test?

The problem i am having is my test gets an object from a dao then passes that obejct to another service. Then I get the hibernate error that I am associating the obejct with two sessions.

How can i get junit with spring to start the session and keep if until the end of the test?

-chris worley

omar said...

Hi,
The problem with your sql assertions is that is quite painful to update them if the database change & very verbose. Something like Unitils will ease a lot this task & make the test more readeable & more independent of the database used.Give it a try.