AbstractTransactionalSpringContextTests meets TestNG

December 12, 2006

Introduction

In the search of an alternative to JUnit, in relation to the testing of our classes, we -Mikel and I- decided to give TestNG a try. This testing framework takes advantage of a useful feature introduced in the release 5.0 of the Java Language: the annotations. In addition, it provides a rich set of facilities to ease the unit and integration testing of Java classes, like groups and data providers. Unfortunately, these are very well explained in the home page of the project, so let´s consider them as being out of the scope in regards to the main thread of this post.

The Problem

What we want to have: test cases running within a transactional context AND take advantage of the TestNG facilities. The former is neatly addressed by a class named AbstractTransactionalSpringContextTests, present in the Spring´s mock package. The latter, well, pretty obvious. What it isn´t so obvious is the way to combine both. Why? TestNG does not provide a direct way to surround the test cases with a transaction, so a rollback action is performed at the end of every test. This way, we are able to avoid side effects between test executions, because one test inserts a row and the next one didn´t expect it, or because a database row is updated between executions breaking some assertions, or because … you get the idea. When we decided to start using TestNG as our main framework for testing, this absence lead us to ask Cedric Beust, the TestNG project leader, about some way to accomplish with the transaction stuff. He responded us very kind and rapidly, recommending to implement the rollbacking in an @AfterMethod method.

TransactionRollbackTearDownTestCase

Note: for this class’s name origin, take a look at this site

Probably Cedric is completely right, but we like how AbstractTransactionalSpringContextTests does its business, and of course, the integration with the Spring´s IoC in order to inject the dependencies to the test classes. For that reason, we have come up with an adapter class, which permits us to run TestNG test cases within the transactional context provided by the Spring class. It is kinda adapter because we change the way the class AbstractTransactionalSpringContextTests is used (i.e. inheriting from it), right now we can extend from the new base class and define our test cases as the usual TestNG manner (i.e. through the use of annotations). It is not an adapter as in the software design patterns.

The class is named TransactionalRollbackTearDownTestCase, and following there is a diagram of its relationships:

TransactionalRollbackTeardownTestCase

The code of this class could prove more descriptive than me trying to explain how it does its job:

package com.centuryminds.test;

import org.springframework.test.AbstractDependencyInjectionSpringContextTests;
import org.springframework.test.AbstractTransactionalSpringContextTests;

import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;

/**
 * Base test class which permits: 1) Inject dependencies defined in a Spring
 * context. The aforementioned dependencies must be declared as protected
 * members 2) Perform a rollback action after every test. Basically, it
 * acts as an adapter between the TestNG test case structure and the facilities
 * provided by the class {@link AbstractDependencyInjectionSpringContextTests}
 * in the Spring package
 *
 * @see AbstractDependencyInjectionSpringContextTests
 * @see AfterMethod
 * @see BeforeMethod
 */
public abstract class TransactionRollbackTearDownTestCase extends
		AbstractTransactionalSpringContextTests
{

	/**
	 * Test Fixture
	 *
	 * @throws Exception if a error occurred during the fixture execution
	 * @see {@link AbstractDependencyInjectionSpringContextTests#setUp()}
	 * @see {@link AbstractDependencyInjectionSpringContextTests#setPopulateProtectedVariables(boolean)}
	 */
	@BeforeMethod
	protected final void before() throws Exception
	{
		setPopulateProtectedVariables(true);
		super.setUp();
	}

	/**
	 * Test Teardown
	 *
	 * @throws Exception if a error occurred during the teardown execution
	 * @see {@link AbstractDependencyInjectionSpringContextTests#tearDown()}
	 */
	@AfterMethod
	protected final void after() throws Exception
	{
		super.tearDown();
	}
}

As it can be seen, this class is really simple. It defines two final methods, both TestNG-annotated which delegate the test fixture and teardown actions to the underlying parent implementation. Therefore, every time a test case method is going to be executed, before() will inject the dependencies defined in the Spring context and will create and start a new transaction context. The method tearDown() in turn will perform a rollback action just after the test ends.

Extending the Hierarchy

Obviously, the class above is not enough: where are the dependencies to be injected defined? We have to tell AbstractTransactionalSpringContextTests where to look for them. This can be achieved by a subclass of TransactionalRollbackTearDownTestCase, for example:

package com.centuryminds.test;

/**
 * Base test class that specifies the spring contexts containing the dao
 * definitions. The subclasses must provide an implementation of the method
 * {@link #getConfigLocations()}, specifying the files with the spring contexts
 * holding the definitions of the dao and its dependencies
 */
public class BaseDAOTestCase extends TransactionRollbackTearDownTestCase
{
	/**
	 * @see org.springframework.test.AbstractDependencyInjectionSpringContextTests#getConfigLocations()
	 */
	@Override
	protected String[] getConfigLocations()
	{
		return new String[] { "classpath:applicationContext-datasource.xml",
				"classpath:applicationContext-service.xml" };
	}
}

The Actual Test Class

One important benefit of the separation above is that we could define several BaseDAO* classes, each one of them declaring its own context set. Let´s see an example DAO test class extending from BaseDAOTestCase:

package com.centuryminds.test;

import java.util.HashSet;

import org.testng.annotations.Test;

import com.threefish.aquarium.dao.hibernate.GenericHibernateDAO;
import com.threefish.aquarium.model.Role;
import com.threefish.aquarium.model.User;

/**
 * Tests for RoleDAO
 */
public class RoleDAOTest extends BaseDAOTestCase
{
	/** role dao */
	protected GenericHibernateDAO roleDAO;

	/** role */
	private Role role;

	@Test
	public void testCreate() throws Exception
	{
		int amount = roleDAO.findAll().size();

		// let's create a new role to persist
		role = new Role();
		role.setName("testrole");
		role.setDescription("Master of Universe");
		role.setUsers(new HashSet());

		roleDAO.makePersistent(role);

		assertNotNull(role.getId());
		assertEquals(amount + 1, roleDAO.findAll().size());
	}

	@Test
	public void testSuccessfulGet() throws Exception
	{
		role = roleDAO.findById(Long.valueOf(1));

		assertEquals("admin", role.getName());
	}

	@Test
	public void testUpdate() throws Exception
	{
		role = roleDAO.findById(Long.valueOf(1));
		role.setDescription("Super Administrator Role");
		roleDAO.makePersistent(role);

		assertEquals("Super Administrator Role", role.getDescription());
	}
}

The RoleDAO test class makes use of a custom generic DAO framework, but despite that I think it is pretty self-explanatory. We shall try to explain the generic DAO related interfaces and classes in another post when we get a chance, but for now we have enough.

The test methods are executed within a transaction per test method, which is rollbacked at the very end of it. So, the role insert in the first test (testCreate()), would be rollbacked and future tests depending on the initial state (i.e. the state before testCreate() ran) wouldn´t be affected. We accomplished with the goal of executing the test cases in isolation. Thanks Spring! We accomplished with the goal of using some of the features offered by TestNG. Thanks to you as well!

Future

We are aware about the drawbacks of this solution, and we are pretty sure about the fact that there must exist a more sophisticated solution out there, hence we are very willing to listen to your suggestions to improve the general structure of our current one. We’ll stay tuned …