Thursday, October 04, 2012

Writing Unit Tests to Ensure Your "@Transactional ... rollbackFor" Annotations are Honoured

Thanks to Russ Hart for providing the info on how to get this to work.  I just cut and paste, and then blogged it.

It's nice to write unit tests.  I wanted to write a set of tests for a method that I'd marked with the Spring @Transactional annotation:


          @Transactional(propagation = Propagation.MANDATORY, rollbackFor = {MessagingException.class})
    @Transformer
    public IncomingEmailDTO receiveMessage(Message message) throws MessagingException {

        IncomingEmailDTO emailDTO = null;
        ...

The tests for the method were simple. But then I realised I wanted to also test that rollbacks were happening or not as required as specified by the "rollbackFor = {MessagingException.class}" part of the annotation.

A quick aside (because I was asked this by the person who gave me the solution to this, and it's entirely valid).  Why did I want to test this?  Aren't I just testing that this Spring annotation works?  In part, yes, this will be the effect of any tests for this.  But there was something else I wanted.  Unit tests, over time, build into a massive, executable spec. for your system.  It would be very easy for someone in the future to change this small part of the annotation (or remove it altogether) and have a very significant effect on the running of the whole system.  Consequently, by adding tests which check that this test is a) transactional, and b) set to rollback for specific exceptions only; I can protect myself against this unfortunate outcome.

But back to the example.  

We had the following Spring XML config:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd">
  
    <context:annotation-config />
    <tx:annotation-driven transaction-manager="transactionManager" />
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="oracle.jdbc.OracleDriver"/>
        <property name="url" value="jdbc:oracle:thin:@localhost:1521:MYSID"/>
        <property name="username" value="ME"/>
        <property name="password" value="ME"/>
    </bean>
</beans>
And then in our unit test we had to have the following:

@ContextConfiguration(locations = {"classpath:spring/appContext-incomingEmailReceiverTest.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
public class IncomingEmailTransformerTest {

    @Autowired
    private PlatformTransactionManager transactionManager;

    @Transactional
    @Test
    public void test_transform_valid_incoming_email_with_no_attachment_works_and_no_rollback()
            throws MessagingException {
    
        IncomingEmailDTO result = emailTransformer.receiveMessage(validInputMessage);

        assertFalse(transactionManager.getTransaction(null).isRollbackOnly());
    }

    @Transactional
    @Test
    public void test_MessagingException_when_extracting_originator_gets_thrown_and_tx_rolls_back()
            throws MessagingException {

        stub(mockEmailDataExtractor.extractOriginator(incomingEmailMessage)).toThrow(new MessagingException());
    
        IncomingEmailDTO result = emailTransformer.receiveMessage(validInputMessage);

        assertTrue(transactionManager.getTransaction(null).isRollbackOnly());
    }


And there we have it.