Tuesday, November 4, 2008

Testing With Spring, Hibernate and JUnit4

This post builds off the functional code and test methods constructed in previous posts. While the techniques in here are not restricted to the features specific to those situations, it might be helpful to review those posts for more context about the original code, the problems that we solved and the patterns that emerged leading to these re-factorings.

Motivation


To use the AbstractTransactionalDataSourceSpringContextTests with JUnit 4 test cases.

The Problem


When our test cases extend AbstractTransactionalDataSourceSpringContextTests, the JUnit 3.8 test runner executes them, so we are not able to use some features available in JUnit 4.

Write a DAO Method and a JUnit 4 Test Case


We will first create a method in the DAO that would benefit from one of the features available in JUnit 4 but not JUnit 3.8, i.e., a verification that the method invocation throws a particular exception deterministically under specific conditions. For the example, the method can simply always throw the expected exception.


public void expectDataAccessException() {
getHibernateTemplate().execute(new HibernateCallback() {
public Object doInHibernate(final Session session) throws HibernateException, SQLException {
throw new SQLException("");
}
});
}



The test case will declare the expectation that the particular exception will be thrown when the method under test is invoked.


@Test(expected = DataAccessException.class)
public void testExpectDataAccessException() throws Exception {
getAuthorDAO().expectDataAccessException();
}



If we run this test now, the test case will fail with an UncategorizedSQLException, even though we have declared our expectation in the annotation.

Re-factor To Composition


The solution is to create a test delegate for Spring-specific configuration (e.g., the setup of the Spring configuration location and the injection of Spring-managed beans) and for Spring datasource transaction management. One of the more powerful features of this particular test base class is that transactions that occur in test cases are rolled back on tear-down, so we generally have no need to worry about test data corruption.


package spring.hibernate.oracle.stored.procedures.dao;

import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;

public class AbstractTransactionalDataSourceSpringContextTestsDelegate extends
AbstractTransactionalDataSourceSpringContextTests {

private AuthorDAO authorDAO;

public void setup() throws Exception {
super.setUp();
}

public void teardown() throws Exception {
super.tearDown();
}

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

public AuthorDAO getAuthorDAO() {
return authorDAO;
}

public void setAuthorDAO(final AuthorDAO authorDAO) {
this.authorDAO = authorDAO;
}
}



Now we are ready to remove the delegate code from the test class (and remove the superclass dependency on the AbstractTransactionalDataSourceSpringContextTests that causes the test to execute with the JUnit 3.8 runner). Note that we must declare @Before and @After methods to setup and tear-down the Spring delegate. Also note that we have retained the test-case specific setup (i.e., the population of Authors). Our class consists only of data setup and tests.


package spring.hibernate.oracle.stored.procedures.dao;

import static org.junit.Assert.assertEquals;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.dao.DataAccessException;
import spring.hibernate.oracle.stored.procedures.domain.Author;

public class AuthorDAOTest {

private final AbstractTransactionalDataSourceSpringContextTestsDelegate delegate;

public AuthorDAOTest() {
delegate = new AbstractTransactionalDataSourceSpringContextTestsDelegate();
}

@Before
public void setUp() throws Exception {
delegate.setup();
createAuthor(1, "Jules", "Verne");
createAuthor(2, "Charles", "Dickens");
createAuthor(3, "Emily", "Dickinson");
createAuthor(4, "Henry", "James");
createAuthor(5, "William", "James");
createAuthor(6, "Henry", "Thoreau");
}

@After
public void tearDown() throws Exception {
delegate.teardown();
}

@Test
public void testFindByLastNameUsingHQL() throws Exception {
assertEquals(2, delegate.getAuthorDAO().findByLastNameUsingHQL("James").size());
assertEquals(1, delegate.getAuthorDAO().findByLastNameUsingHQL("Verne").size());
assertEquals(1, delegate.getAuthorDAO().findByLastNameUsingHQL("Dickinson").size());
assertEquals(1, delegate.getAuthorDAO().findByLastNameUsingHQL("Dickens").size());
assertEquals(0, delegate.getAuthorDAO().findByLastNameUsingHQL("Whitman").size());
}

@Test
public void testFindByLastNameUsingStoredProcedure() throws Exception {
assertEquals(2, delegate.getAuthorDAO().findByLastNameUsingStoredProcedure("James").size());
assertEquals(1, delegate.getAuthorDAO().findByLastNameUsingStoredProcedure("Verne").size());
assertEquals(1, delegate.getAuthorDAO().findByLastNameUsingStoredProcedure("Dickinson").size());
assertEquals(1, delegate.getAuthorDAO().findByLastNameUsingStoredProcedure("Dickens").size());
assertEquals(0, delegate.getAuthorDAO().findByLastNameUsingStoredProcedure("Whitman").size());
}

@Test
public void testFindByFirstNameUsingFunction() throws Exception {
assertEquals(0, delegate.getAuthorDAO().findByFirstNameUsingFunction("James").size());
assertEquals(2, delegate.getAuthorDAO().findByFirstNameUsingFunction("Henry").size());
}

@Test
public void testUpdate() throws Exception {
final Author author = delegate.getAuthorDAO().findByLastNameUsingHQL("Thoreau").get(0);
author.setLastName("Miller");
delegate.getAuthorDAO().update(author);
assertEquals(1, delegate.getAuthorDAO().findByLastNameUsingHQL("Miller").size());
}

@Test(expected = DataAccessException.class)
public void testExpectDataAccessException() throws Exception {
delegate.getAuthorDAO().expectDataAccessException();
}

private void createAuthor(final int id, final String firstName, final String lastName) {
delegate.getJdbcTemplate().execute(
String.format("insert into author (id, first_name, last_name) values (%d, '%s', '%s')", id, firstName, lastName));
}
}



Observe Lifecycle Events


In order to listen for transaction-specific lifecycle events, we will create an Observer, effectively re-factoring method override to method observation.


package spring.hibernate.oracle.stored.procedures.dao;

public interface ITransactionListener {

void onSetUpBeforeTransaction() throws Exception;

void onSetUpInTransaction() throws Exception;

void onTearDownInTransaction() throws Exception;

void onTearDownAfterTransaction() throws Exception;
}



In the delegate, we now need to broadcast the transaction lifecycle events.


....
private final Collection transactionListeners;

public AbstractTransactionalDataSourceSpringContextTestsDelegate() {
transactionListeners = new ArrayList();
}
....
public void registerTransactionListener(final ITransactionListener listener) {
transactionListeners.add(listener);
}

@Override
protected void onSetUpBeforeTransaction() throws Exception {
super.onSetUpBeforeTransaction();
for (final ITransactionListener listener : transactionListeners)
listener.onSetUpBeforeTransaction();
}

@Override
protected void onSetUpInTransaction() throws Exception {
super.onSetUpInTransaction();
for (final ITransactionListener listener : transactionListeners)
listener.onSetUpInTransaction();
}

@Override
protected void onTearDownInTransaction() throws Exception {
try {
for (final ITransactionListener listener : transactionListeners)
listener.onTearDownInTransaction();
} finally {
super.onTearDownInTransaction();
}
}

@Override
protected void onTearDownAfterTransaction() throws Exception {
try {
for (final ITransactionListener listener : transactionListeners)
listener.onTearDownAfterTransaction();
} finally {
super.onTearDownAfterTransaction();
}
}



We can easily create a tracing transaction listener to demonstrate how this lifecycle observation works.


package spring.hibernate.oracle.stored.procedures.dao;

public class TracingTransactionListener implements ITransactionListener {

public void onSetUpBeforeTransaction() throws Exception {
System.out.println("TracingTransactionListener.onSetUpBeforeTransaction()");
}

public void onSetUpInTransaction() throws Exception {
System.out.println("TracingTransactionListener.onSetUpInTransaction()");
}

public void onTearDownAfterTransaction() throws Exception {
System.out.println("TracingTransactionListener.onTearDownAfterTransaction()");
}

public void onTearDownInTransaction() throws Exception {
System.out.println("TracingTransactionListener.onTearDownInTransaction()");
}
}



We will add this listener to the delegate on test setup. Note that the listener must be registered before the delegate itself is setup.


....
@Before
public void setUp() throws Exception {
delegate.registerTransactionListener(new TracingTransactionListener());
delegate.setup();
....
}



If we run the this test class now, we should note console output that includes these statements:
  • TracingTransactionListener.onSetUpBeforeTransaction()
  • TracingTransactionListener.onSetUpInTransaction()
  • TracingTransactionListener.onTearDownInTransaction()
  • TracingTransactionListener.onTearDownAfterTransaction()

4 comments:

Ariel Valentin said...

Why not use the new Spring 2.5+ annotations?

Tim Myer said...

Good to hear from you. Which annotations are you referring to?
Thanks.
------Tim------

Ariel Valentin said...

Example taken from:
http://static.springframework.org/spring/docs/2.5.x/reference/testing.html

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@TransactionConfiguration(transactionManager="txMgr", defaultRollback=false)
@Transactional
public class FictitiousTransactionalTest {

@BeforeTransaction
public void verifyInitialDatabaseState() {
// logic to verify the initial state before a transaction is started
}

@Before
public void setUpTestDataWithinTransaction() {
// set up test data within the transaction
}

@Test
// overrides the class-level defaultRollback setting
@Rollback(true)
public void modifyDatabaseWithinTransaction() {
// logic which uses the test data and modifies database state
}

@After
public void tearDownWithinTransaction() {
// execute "tear down" logic within the transaction
}

@AfterTransaction
public void verifyFinalDatabaseState() {
// logic to verify the final state after transaction has rolled back
}

@Test
@NotTransactional
public void performNonDatabaseRelatedAction() {
// logic which does not modify database state
}
}

Tim Myer said...

Hi Ariel,
Excellent! Thank you for the link.
I agree that in the general case, the Spring 2.5+ annotations appear to be a good way to go. However I can think of at least one situation where these annotations might be problematic and using a composite would be preferable: when you need more than one JUnit runner. In fact, there appears (as far as I can tell) to be a similar limitation with @RunWith and with test base class extension -- namely, that we can use only one test runner and we can use only one base class.
For example, one test pattern I have noticed over my last 3 projects is the early use of mock Objects (in particular, JMock), but as components are fully implemented, these mock Objects eventually make way for their functional components.
JMock uses its own JUnit runner similarly to Spring, as in this example:
....
@RunWith(JMock.class)
public class AuthorDAOTest {

private final Mockery mockery = new Mockery() {
{
setImposteriser(ClassImposteriser.INSTANCE);
}
};

....
@Test
public void testWithMockery() throws Exception {
final AuthorDAO dao = delegate.getAuthorDAO();
final HibernateTemplate mock = mockery.mock(HibernateTemplate.class);
mockery.checking(new Expectations() {
{
one(mock).find("from Author author where author.lastName = ?", "Thoreau");
one(mock).bulkUpdate("something here");
}
});
dao.setHibernateTemplate(mock);
dao.findByLastNameUsingHQL("Thoreau");
}
....

The test case succeeds without the runner but fails with it because the runner performs validations on teardown (and the DAO method never calls bulkUpdate).

I could certainly imagine a scenario where I would want to use Mock Objects in a test class alongside Spring transactional components.

I would be curious to hear about other possible solutions.

Thanks for getting me to look further into this and for introducing me to Spring test annotations!
-----Tim-----