Unit testing Spring/Hibernate code using JMock
Unit testing a simple Java class like, say, a four-function calculator is trivially easy using JUnit. However, things get a lot harder when you’re using complex support frameworks like Spring or Hibernate. Here are some guidelines for using JMock to help isolate the framework and focus on the code you’re testing.
Before you start, take a minute to consider whether it’s truly a unit test you’re looking for. The key question is whether your code is really independent from the environment, or if it depends on some clever capability of the framework or database. For example, suppose it generated some complex SQL and then used the results of that SQL; you should consider whether your tests might need to interact with a real database to be sure that your code is correct. If that’s the case, then you probably shouldn’t be using mocks.
Mocking objects in java is actually quite hard to do (much harder than Ruby or Smalltalk) but JMock helps a lot. JMock (http://www.jmock.org) has improved hugely in version 2 – it’s practically a rewrite from version 1. Also, I’m going to use JUnit 4 which allows us to use annotations.
Let’s assume we’ve just written Shipper class with a method that computes the weight of a shipment by adding the weights of the individual components and the weight of a standard shipping box. There’s a catalog service that can retrieve the weight of a component and a config service that stores the standard shipping box weight. Spring is used to wire together the services and Hibernate is used to persist the data. Here’s what our class might look like (the database locking is just to make it interesting):
package com.sample;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.LockMode;
import org.springframework.orm.hibernate3.SessionFactoryUtils;
public class Shipper {
Config config;
Catalog catalog;
SessionFactory sessionFactory;
public void setConfig(Config config) {
this.config = config;
}
public void setCatalog(Catalog catalog) {
this.catalog = catalog;
}
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
public void updateTotalWeight(Shipment shipment, String[] ids) {
Session session = SessionFactoryUtils.getSession(sessionFactory, true);
session.refresh(shipment, LockMode.UPGRADE_NOWAIT);
int weight = config.getStandardShippingWeight();
if (ids != null) {
for (String id : ids) {
weight += catalog.getWeight(id);
}
}
shipment.setWeight(weight);
}
}
Now let’s look at the test code for this class (I’ve deleted most comments from both files to make them easier to follow). Our objective in the test is to verify the ‘happy route’ through the method, as well as the edge cases with a null or empty list:
package com.sample;
import static org.junit.Assert.assertEquals;
import org.junit.runner.RunWith;
import org.junit.Before;
import org.junit.Test;
import org.jmock.integration.junit4.JMock;
import org.jmock.integration.junit4.JUnit4Mockery;
import org.jmock.Mockery;
import org.jmock.Expectations;
import org.jmock.lib.legacy.ClassImposteriser;
import org.hibernate.SessionFactory;
import org.hibernate.Session;
@RunWith(JMock.class)
public class ShipperTestCase {
Mockery context;
Shipper shipper;
Catalog catalog;
Config config;
SessionFactory sessionFactory;
Session session;
@Before
public void prepareMocks() {
context = new JUnit4Mockery() {
{
// Enable mocks of concrete classes
setImposteriser(ClassImposteriser.INSTANCE);
}
};
catalog = context.mock(Catalog.class);
config = context.mock(Config.class);
sessionFactory = context.mock(SessionFactory.class);
session = context.mock(Session.class);
// Create our test object and wire up mock services to it
shipper = new Shipper();
shipper.setCatalog(catalog);
shipper.setConfig(config);
shipper.setSessionFactory(sessionFactory);
}
@Test
public void emptyOrNullList() {
Shipment shipment = new Shipment();
// Set up the expected behaviour in the support services
context.checking(new Expectations() {
{
allowing(sessionFactory).openSession(); will(returnValue(session));
allowing(session).getSessionFactory(); will(returnValue(sessionFactory));
ignoring(session);
// Set required expectations for test to pass
atLeast(1).of(config).getStandardShippingWeight(); will(returnValue(8));
}
});
shipper.updateTotalWeight(shipment, null);
assertEquals(8, shipment.getWeight());
shipment.setWeight(0);
shipper.updateTotalWeight(shipment, new String[0]);
assertEquals(8, shipment.getWeight());
}
@Test
public void sampleList() {
Shipment shipment = new Shipment();
// Set up the expected behaviour in the support services
context.checking(new Expectations() {
{
allowing(sessionFactory).openSession(); will(returnValue(session));
allowing(session).getSessionFactory(); will(returnValue(sessionFactory));
ignoring(session);
// Set required expectations for test to pass
atLeast(1).of(config).getStandardShippingWeight(); will(returnValue(4));
atLeast(1).of(catalog).getWeight("a"); will(returnValue(1));
atLeast(1).of(catalog).getWeight("b"); will(returnValue(2));
atLeast(1).of(catalog).getWeight("c"); will(returnValue(4));
}
});
shipper.updateTotalWeight(shipment, new String[] { "a", "b", "c"});
assertEquals(15, shipment.getWeight());
}
}
Let’s look at a few interesting parts of the test code:
@RunWith(JMock.class)
public class ShipperTestCase {
This causes JUnit to use the runner from JMock, instead of its built-it one.
@Before
public void prepareMocks() {
context = new JUnit4Mockery() {
{
setImposteriser(ClassImposteriser.INSTANCE); // Enable mocks of concrete classes
}
};
catalog = context.mock(Catalog.class);
config = context.mock(Config.class);
sessionFactory = context.mock(SessionFactory.class);
session = context.mock(Session.class);
// Create our test object and wire up mock services to it
shipper = new Shipper();
shipper.setCatalog(catalog);
shipper.setConfig(config);
shipper.setSessionFactory(sessionFactory);
}
This method runs before each of the test cases. It’s (hopefully) very straightforward – it creates mock objects for each of the external services that our class relies on, and then creates an object to test with those mocks wired to it.
// Set up the expected behaviour in the support services
context.checking(new Expectations() {
{
allowing(sessionFactory).openSession(); will(returnValue(session));
allowing(session).getSessionFactory(); will(returnValue(sessionFactory));
ignoring(session);
// Set required expectations for test to pass
atLeast(1).of(config).getStandardShippingWeight(); will(returnValue(4));
atLeast(1).of(catalog).getWeight("a"); will(returnValue(1));
atLeast(1).of(catalog).getWeight("b"); will(returnValue(2));
atLeast(1).of(catalog).getWeight("c"); will(returnValue(4));
}
});
This block sets the behaviour we want from our mocks. Notice that we can specify different behaviour based on different parameters to a method. The ignoring(session) line indicates that all other methods of session should simply return a default value.
shipper.updateTotalWeight(shipment, new String[] { "a", "b", "c"});
assertEquals(15, shipment.getWeight());
Finally, we call our test object and verify the results.
When writing expectations, it’s worth making sure you only set constraints (like the atLeast(1) line) where a mock method MUST be called or you consider the test as failed. If you make the constraints too tight, then your tests can get brittle and can fail when you change the implementation, even if that change hasn’t broken the functionality. For example, if I’d used exactly(1) instead of atLeast(1), the test would still pass but would be less resilient to implementation changes.