Thursday, July 15, 2010

Patterns for Better Unit Testing with JPA

Over the years I’ve run across many projects, which tend to fall into one of two categories: those projects that have great confidence, energy and momentum, running hundreds (or thousands) of unit tests dozens of times every day; and those projects where the effort of writing and running unit tests is high enough that tests aren’t maintained and as a result no one on the team really knows if their software works. So how do we get our project into the first category? Below I highlight a few patterns and practices that significantly lower the bar to creating a solid test suite, enabling your team to adopt a practice and culture of complete test coverage.

Setup and Speed

Configuration, setup and maintenance of environments creates a significant barrier to a well-exercised and maintained test suite. Ideally we want to enable running tests with zero setup. Data-intensive applications need a database, which are notoriously hard to configure and setup. We’ll use a database for our tests, but instead of connecting to an external one the database will start up and run as part of the unit test. This gives us the following:

  • zero setup: no installation, no shared machine, no maintenance, no JDBC urls, no passwords
  • in-memory and fast
  • can run anywhere, even when not on a network
  • clean, with no rogue data that could affect our tests
  • no deadlocks

My first reaction when I saw this approach in use was “how can tests be meaningful if they're not run on the same database software that is used in production”? The answer is two-sided. We should be running these tests on the same database software. In fact, I encourage you to do that — on your continuous integration build server. The tests are meaningful in that they can make assertions and exercise your code. Database platform-specific issues can be flushed out on the build server, which can run these same tests using a different configuration.

To set this up I recommend using Apache Derby or Hypersonic. Here’s how it’s done with Hypersonic and Eclipselink as our JPA provider:

  1. add hsqldb.jar to the test project classpath (this is for Hypersonic)
  2. configure your JDBC connection to use an in-memory database URL as follows: jdbc:hsqldb:mem:tests
  3. configure your persistence.xml with the right SQL dialect. For Eclipselink this is done by setting eclipselink.target-database to HSQL as follows:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="blogDomain" transaction-type="RESOURCE_LOCAL">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
... snip ...
<properties>
<property name="eclipselink.target-database" value="HSQL"/>
<property name="eclipselink.ddl-generation" value="create-tables"/>
<property name="eclipselink.ddl-generation.output-mode"
value="database"/>
<property name="eclipselink.weaving" value="false"/>
<property name="eclipselink.logging.level" value="INFO"/>
</properties>
</persistence-unit>
</persistence>

With this configuration the test suite will create a new in-memory database every time it’s run, and Eclipselink will create the necessary tables to persist our data. We’ve managed to eliminate the need for any kind of setup or external database to run our tests. That’s great, but now we’ve got an empty database, where does our data come from?

Test Data

Data-intensive applications need data for their tests — that’s a given. For our tests to run reliably and check specific scenarios every time, we need the data used by each test case to be the same every time tests are run. The best way to do this is to have our tests mock up the data. Mocking data can be cumbersome, however that can be alleviated by using mock factories for our domain objects. Mock factories populate domain objects with values such that they’re ready to use as-is. Where needed, tests can alter the state of domain objects to create the desired scenario. Here’s an example:

 @Test
public void testUpdateBlog() {
Blog blog = MockFactory.on(Blog.class).create(entityManager);
entityManager.flush();
entityManager.clear();

blog.setName(blog.getName()+"2");

Blog updatedBlog = service.updateBlog(blog);
assertNotNull(updatedBlog);
assertNotSame(updatedBlog,blog);
assertEquals(blog.getName(),updatedBlog.getName());
assertEquals(blog.getId(),updatedBlog.getId());
}

In this example the test verifies that the Blog entity is properly updated by the service. Notice that the test didn’t have to populate the blog object before persisting it: all of the required values were filled in by the MockFactory.

For this to work, we’ll need a MockFactory implementation. This is what ours looks like:

/**
* A factory for domain objects that mocks their data.
* Example usage:
* <pre><code>
* Blog blog = MockFactory.on(Blog.class).create(entityManager);
* </code></pre>
* @author David Green
*/
public abstract class MockFactory<T> {

private static Map<Class<?>,MockFactory<?>> factories =
new HashMap<Class<?>, MockFactory<?>>();
static {
register(new MockBlogFactory());
register(new MockArticleFactory());
}
private static void register(MockFactory<?> mockFactory) {
factories.put(mockFactory.domainClass,mockFactory);
}
@SuppressWarnings("unchecked")
public static <T> MockFactory<T> on(Class<T> domainClass) {
MockFactory<?> factory = factories.get(domainClass);
if (factory == null) {
throw new IllegalStateException(
"Did you forget to register a mock factory for "+
domainClass.getClass().getName()+"?");
}
return (MockFactory<T>) factory;
}

private final Class<T> domainClass;

private int seed;

protected MockFactory(Class<T> domainClass) {
if (domainClass.getAnnotation(Entity.class) == null) {
throw new IllegalArgumentException();
}
this.domainClass = domainClass;
}

/**
* Create several objects
* @param entityManager the entity manager, or null if the mocked objects
* should not be persisted
* @param count the number of objects to create
* @return the created objects
*/
public List<T> create(EntityManager entityManager,int count) {
List<T> mocks = new ArrayList<T>(count);
for (int x = 0;x<count;++x) {
T t = create(entityManager);
mocks.add(t);
}
return mocks;
}

/**
* Create a single object
* @param entityManager the entity manager, or null if the mocked object
* should not be persisted
* @return the mocked object
*/
public T create(EntityManager entityManager) {
T mock;
try {
mock = domainClass.newInstance();
} catch (Exception e) {
// must have a default constructor
throw new IllegalStateException();
}
populate(++seed,mock);
if (entityManager != null) {
entityManager.persist(mock);
}
return mock;
}

/**
* Populate the given domain object with data
* @param seed a seed that may be used to create data
* @param mock the domain object to populate
*/
protected abstract void populate(int seed, T mock);


private static class MockBlogFactory extends MockFactory<Blog> {
public MockBlogFactory() {
super(Blog.class);
}

@Override
protected void populate(int seed, Blog mock) {
mock.setName("Blog "+seed);
}
}

private static class MockArticleFactory extends MockFactory<Article> {

public MockArticleFactory() {
super(Article.class);
}

@Override
protected void populate(int seed, Article mock) {
mock.setAuthor("First Last");
mock.setTitle("Article "+seed);
mock.setContent("article "+seed+" content");
}
}
}

This implementation uses static methods and a static initializer. It’s not very Spring-like. If you prefer you could get rid of all the statics and instead have Spring inject (autowire) your mock factories into your test classes.

In our implementation we use an integer seed to help us produce values that vary. This implementation is fairly trivial, however if needed we could apply more advanced techniques such as using data dictionaries to source data. Such mock factories should populate enough values that the mocked domain object can be persisted: all not-null properties should have values. To ensure that this is true over the lifespan of our project, it’s important to have a test:

/**
* test that {@link MockFactory mock factories} are working as expected
*
* @author David Green
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration( { "/applicationContext-test.xml" })
@Transactional
public class MockFactoryTest {

@PersistenceContext
private EntityManager entityManager;

@Test
public void testCreateBlog() {
Blog blog = MockFactory.on(Blog.class).create(entityManager);
assertNotNull(blog);
assertNotNull(blog.getName());
entityAssertions(blog);
entityManager.flush();
}
@Test
public void testCreateArticle() {
Blog blog = MockFactory.on(Blog.class).create(entityManager);
Article article = MockFactory.on(Article.class).create(null);
article.setBlog(blog);
entityManager.persist(article);
assertNotNull(article);
assertNotNull(article.getTitle());
assertNotNull(article.getContent());
entityAssertions(article);
entityManager.flush();
}
private void entityAssertions(AbstractEntity entity) {
assertNotNull(entity.getId());
assertNotNull(entity.getCreated());
assertNotNull(entity.getModified());
}
}

The key behind this approach is that each test mocks its own data. Where data models are signifcantly more complex, utility functions can use these mock factories to create common scenarios.

Now that we’ve got an easy way to produce data in our tests, how do we eliminate potential for side-effects between tests? That part is easy: we ensure that transactions are rolled back after every test. In our example we’re using Spring to run our tests via SpringJUnit4ClassRunner. Its default behaviour is to roll back after each test is run, however if you’re not using Spring you should have something like this in an abstract test case class:

 /**
* Overriding methods should call super.
*/
@After
public void after() {
if (entityManager.getTransaction().isActive()) {
entityManager.getTransaction().rollback();
}
}

Summary

We’ve seen how we can make unit testing easy with a few simple patterns:

  • in-memory database
  • mocked data
  • roll back transactions after each test

By employing these simple techniques, tests are easy to write and run. Your team will get addicted to thorough test coverage as the number of tests being run increases and provides tangible feedback of their progress.

In my next article I’ll post complete working source code and show how these techniques can be taken a step further in testing Spring REST web services.

9 comments:

Mike W said...

I like the abstract base class approach for instantiation of the mock objects and the inevitable utility methods that seem to always be necessary. I suppose you could even subclass it further for different superclasses of mock objects you wanted to create.

I guess using private inner classes is appropriate for this example but a real project would need one separate class for each entity you wanted to mock.

Anonymous said...

Did you try H2 instead of HSQLDB / Derby?

wytten said...

Another vote for H2. Their web server makes it easy to inspect the contents of the in-memory data while you debug the test. See Example

Anonymous said...

Why reinvent another Mock FW? Why dont you use something like Mockito or EasyMock?

David Green said...

@Anonymous I'm sure that H2 would work just as well.

@Anonymous regarding Mockito and EasyMock: these are both good at what they're intended to do, but (unless I'm missing something) don't provide a lot of lift for providing data for tests using JPA.

sud said...

An alternate approach is to use DBUnit to seed the in-memory database as part of the test setup.

And for Spring based projects use the AbstractTransactionalJUnit4SpringContextTests to provide automatic rollback on transactions at the end of a test.

David Green said...

@sud there's no need for AbstractTransactionalJUnit4SpringContextTests in order to rollback your test transactions: Spring has @TransactionConfiguration and when left unspecified the defaultRollback defaults to true -- in other words there's nothing to do to have Spring rollback your transactions by default. If you're interested to see how this works, take a look at TransactionalTestExecutionListener.

Peng Fei Xu said...

Very nice article. Still very useful now. I can not believe it was written two years again. Thanks.

Applicius said...

Still an interesting testing topic. Acolyte framework has been recent developed in order to be a new tool for that: http://applicius-en.tumblr.com/post/55081625831/persistence-unit-test .