søndag den 15. januar 2012

How to get Spring transactions to work with the JUnit Theories testrunner

Yes, I will make an exception and write this in English because I think the subject might have broader interest: A solution to the challenge of combining the JUnit Theories testrunner with Spring transaction support.

I am not going to say a lot about JUnit Theories (or @Rule that matter); you can google plenty of good information about that yourself. I am definitely NOT going to even try to talk about how to get Spring transactions to work with your database of choice. And I am not going to argue that it makes much sense to combine Theories and transactional databases. But, hey, challenges doesn't have to make sense to be interesting, right? :-)

And this is a challenge, because Springs support for unittest almost only works with the Spring testrunner and Theories only works if you use the Theories testrunner - and you can only have one testrunner per test. But luckily there is a way out.

So, lets see some code - and lets start with this fairly simple Spring based test:

@ContextConfiguration(locations = "classpath:testcontext.xml")
@Transactional
@RunWith(SpringJUnit4ClassRunner.class)

public class SimpleTest {
@Autowired DataSource datasource;

private JdbcTemplate jdbcTemplate;

@Before public void setup() throws SQLException {
jdbcTemplate = new JdbcTemplate(datasource);
}

@Test
public void canInsert() throws SQLException {
jdbcTemplate.update("INSERT INTO test( id ) VALUES (?)", 1);
}
}

Note that it uses the Spring testrunner and that it wants to be transactional. Doing so makes Spring to some pretty nifty magic behind the scenes, so that everything you do to the database in a single test is wrapped in a transaction that is rolled back afterwards. Spring has even thought of starting the transaction before the @Before method and will first end it after the @After method, which is pretty clever.

But it all hinges on using the the Spring testrunner. So what if you want to use another testrunner like for instance Theories?

Well, the Spring testrunner delegates to another Spring class called TestContextManager and a fairly common workaround therefore is to do the same delegation in the @Before method as Spring would do in the testrunner. Just like this:

@ContextConfiguration(locations = "classpath:testcontext.xml")
@Transactional
@RunWith(Theories.class)
public class BuggedTheoriesTest {
@Autowired DataSource datasource;

private JdbcTemplate jdbcTemplate;

@Before public void setup() throws Exception {
//here we hook into the Spring test-support framework
final TestContextManager tcm = new TestContextManager(getClass());
tcm.prepareTestInstance(this);

//this will work for everything but spring transactions as transactions expects to be enabled before @Before

jdbcTemplate = new JdbcTemplate(datasource);
}

@Test
public void canInsert() throws SQLException {
jdbcTemplate.update("INSERT INTO test( id ) VALUES (?)", 1);
}
}

The good news is, that it actually manages to get the spring context loaded so the database is accessible. The bad news is, that we're just a tad to late for transactions to work. Remember that they were supposed to start before the @Before method? Well, the downside here is that we're too late when we try to call upon the help of Spring in the @Before method it self.

My googling skills didn't find any solution to this problem, which probably goes to say a lot about my googling skills. So I came up with the solution below (the parts of which I have found by googling :-)). I am not going to say it is particularly clever or even pretty, but hey, it seems to work!

It is based on using the @Rule annotation that was introduced in JUnit 4.7. I am not sure why it is called a "rule", but it is a mechanism that enabled you to define a wrapper that will wrap every invocation of testmethods in the test including the @Before and @After parts.

@ContextConfiguration(locations = "classpath:testcontext.xml")
@Transactional
@RunWith(Theories.class)
public class FixedTheoriesTest {
@Autowired DataSource datasource;

private JdbcTemplate jdbcTemplate;

@Rule public SpringTestContextManagerRule springTestContextManagerRule = new SpringTestContextManagerRule();

@Before public void setup() throws Exception {
jdbcTemplate = new JdbcTemplate(datasource);
}

@DataPoints public static int[] ids() {
return new int[] {1,2,5,10,20,50,100,200,500};
}

@Theory
public void canInsert(int id) throws SQLException {
jdbcTemplate.update("INSERT INTO test( id ) VALUES (?)", id);
}
}

That's all there is too it: Spring with transactions working with JUnit Theories!

Oh, and yes, you'll probably need this as well :-D :

public class SpringTestContextManagerRule implements MethodRule{

@Override
public Statement apply(final Statement statement, final FrameworkMethod frameworkMethod, final Object testInstance) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
final TestContextManager tcm = new TestContextManager(testInstance.getClass());
tcm.prepareTestInstance(testInstance);
tcm.beforeTestMethod(testInstance, frameworkMethod.getMethod());
try {
statement.evaluate();
tcm.afterTestMethod(testInstance, frameworkMethod.getMethod(), null);
} catch (Throwable t ) {
tcm.afterTestMethod(testInstance, frameworkMethod.getMethod(), t);
throw t;
}
}
};

}
}

Notice how this Rule makes a Statement that has an evaluate()-method that wraps around the statement passed to it. In that way rules can make statements that wraps statements of other rules, eventually leading to a statement wrapping an actual testmethod.

There is one caveat, though: The implementation is based on a MethodRule; this has been deprecated in later versions of JUnit, and is supposed to be replaced with a TestRule. The only problem with the new TestRule is that it will only provide you with the name of the test method to be invoked, not a reference to the method itself. It should be possible to work around this, but it is easier to use a MethodRule regardless of the deprecation warning.

I have uploaded a maven-based project to github. It should be fairly self-contained, the only real assumption is, that there is a MySQL database on localhost with a schema called testdb that can be used by the user "test".