Unit testing Spring/Hibernate code using JMock
Feb 27th, 2008 by denis
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.
Thanks for writing up the example, you make some good points. There are a few adjustments that strike me as worth thinking about.
Shipper is doing two things at once, calculating the shipment and managing the database. Would it be worth extracting out the database stuff into some kind of helper object?
My preference here would be to stub the calls to getWeight(), rather than assert them. Our convention is to stub queries that don’t change the state of the outside world. Ironically, in the example, the only change that affects a neighbour is to set the shipment weight, which is just a value object. It would be interesting to see an interface for something that receives an update from the Shipper, something like a ShipmentUpdateListener, for example.
A habit I’ve got into is to set up as many values, including mocks, as possible as final field initialisations. It cuts down the syntax noise and helps me clarify the dependencies between instances.
@Steve: Thanks for the comments. The example Shipper class is just made up to illustrate the use of JMock - the original version just calculated and returned the weight. That was a cleaner design but I wanted to show some database access as well as Spring wiring.
I also agree about using stubs vs constraints on calls that don’t have a side effect. I wanted to show that part of your test could be that a mock object was called in a particular way, rather than simply relying on assert() calls after the test.
Cheers, Denis