Category Archives: Testing

Integration Testing Transactional Methods with Spring Boot

Consider a service method in your Spring Boot project that both writes to a queue and a database. If your write to the queue is successful, yet the database call throws an exception, you might not want to keep that item on the queue. Writing the code for this yourself could be cumbersome. Fortunately, Spring provides the handy @Transactional annotation. In this post we’ll look at how to write an integration test for methods using this annotation.

Let’s start with an empty template class for integration test:

@SpringApplicationConfiguration(classes = { Application.class, TestConfig.class })
@WebAppConfiguration
@DirtiesContext
@Slf4j
public class OurCustomServiceIT extends AbstractTestNGSpringContextTests {

}

@SpringApplicationConfiguration loads and configures an ApplicationContext for integration tests. It’s similar to @ContextConfiguration but uses Spring Boot’s SpringApplicationContextLoader. We provide the Application and TestConfig classes. Application is your main Spring Boot entry-point class that contains SpringApplication.run. TestConfig is a custom class we’ll use later to provide mocks for our test.

Use @WebAppConfiguration over @WebIntegrationTest in this example since we will not be testing the HTTP entrypoint into our application (through REST).

@DirtiesContext is necessary here. What it does is mark the ApplicationContext as dirty, thus requiring it to be reloaded for the next integration test. We are going to be using a mocked bean. Since Spring caches the ApplicationContext between test runs, we want to ensure it is not cached with this mocked bean.

@Slf4j is a lombok convenience annotation to prevent us from typing the long-winded:

private static final Logger LOG = LoggerFactory.getLogger(OurCustomServiceIT.class);

Finally extending AbstractTestNGSpringContextTests sets our ApplicationContext up for support with a TestNG environment. Alternatively, you could extend AbstractJUnit4SpringContextTests if you want to use JUnit4.

Now what did we need that TestConfig for?

@Configuration
public class TestConfig {
    @Mock
    RabbitOperations rabbitOperations;

    public TestConfig() {
        MockitoAnnotations.initMocks(this);
    }

    @Bean
    public RabbitOperations rabbitTemplateWithExchange() {
        return rabbitOperations;
    }
}

@Configuration marks our class for use by the Spring context to gather beans.

What we’re doing here is creating a mocked implementation of our queue client. Why do we want a mocked version? Because we are going to simulate the queue client throwing an exception. We want to ensure that when this exception is thrown, that the database entry is not persisted. Indeed, we want the database transaction to rollback.

Now, let’s add the internals to our integration test:

@SpringApplicationConfiguration(classes = { Application.class, TestConfig.class })
@WebAppConfiguration
@DirtiesContext
@Slf4j
public class OurCustomServiceIT extends AbstractTestNGSpringContextTests {
    @Autowired
    private OurCustomService service;

    @Autowired
    private OurCustomRepository repository;

    @Autowired
    @Qualifier("rabbitTemplateWithExchange")
    private RabbitOperations mockedRabbitTemplateWithExchange;

    @Test
    public void testMethodIsTransactional() {
        final String name = "foo";
        doThrow(new RuntimeException("Fake RabbitMQ Failure!")).when(mockedRabbitTemplateWithExchange).send(any(String
                .class));
        try {
            service.addRequestToQueue(name);
        }
        catch (RuntimeException e) {
            LOG.info("Expected failure to trigger transaction caught");
        }

        List<DatabaseRecords> results = repository.getRecords().stream()
                .filter(record -> record.getName().equals(name))
                .collect(Collectors.toList());

        assertThat(results).hasSize(0);
    }
}

We inject our service and repository. Next, we inject our mocked queue client. I use the @Qualifier so I can give the variable name a prefix of “mocked” (as in mockedRabbitTemplateWithExchange). Make sure that people know you are using a mocked version both in this test AND inside your spring boot application.

Finally, the test tells the queue client to throw an exception when it is used. Since it threw an exception, we don’t want our database write to persist – even though the exception occurs after the database call has been made. We check the records in the database to ensure that it has indeed not been written.

Let’s make a red, failing implementation:

public void addRequestToQueue(String name) {
    repository.write(name);
    rabbit.send(name);
}

There’s no @Transactional annotation telling the database to rollback when the queue client throws a RuntimeException.

Finish things up by adding a @Transactional annotation to the addRequestToQueue method and your test should pass with green.