Automatic Rollback of Transactions in Spring Tests

Table of Contents

TL;DR

Put @Transactional on your test class or methods and test-managed transactions will automatically be rollbacked.

The Antipattern

When looking at system test in Spring, it is not uncommon to see this pattern:

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit.jupiter.SpringExtension

@ExtendWith(SpringExtension::class)
@SpringBootTest
class TransactionalTestApplicationTests {

    @Autowired lateinit var mapper: Mapper

    @Test
    fun `insert user into db, verify and do cleanup`() {
        val user = mapper.createUser(username = "foo")

        assertEquals("foo", user.username)

        mapper.deleteUser(username = "foo")
    }
}

The cleanup is done manually, at the end of the test. This introduces a problem: what if the test fails? Now, the cleanup statement at the end is no longer executed. A common solution is to introduce an @After method that does the cleanup in a more or less crude manner:

@After
fun cleanup() {
    mapper.deleteAllUsers()
}

This pattern is in fact unnecessary. Spring provides the @Transactional annotation that will do an automatic cleanup after every @Test method:

@ExtendWith(SpringExtension::class)
@SpringBootTest
@Transactional
class TransactionalTestApplicationTests {

    @Autowired lateinit var mapper: Mapper

    @Test
    fun `insert user into db, verify and do cleanup`() {
        /* code */
    }
}

Difference Between Test-, Spring- and Application-Managed Transactions

A test-managed transaction is one that is managed by TransactionalTestExecutionListener (or programmatically by TestTransaction). A test-managed transaction differs from Spring-managed transactions (transactions directly managed by Spring within the ApplicationContext that is loaded for the test) and application-managed transactions (transactions programmatically managed within the application code that is executed by the test):

Spring-managed and application-managed transactions will typically participate in test-managed transactions; however, caution should be taken if Spring-managed or application-managed transactions are configured with any propagation type other than REQUIRED or SUPPORTS.

Documentation for TransactionalTestExecutionListener

What Does @Transactional Do?

Running tests while starting up the Spring application context, a TransactionalTestExecutionListener is automatically configured. This listener provides support for executing tests with test-managed transactions. It notices the @Transactional on the test class or individual @Test methods and creates a new transaction that is then automatically rolled back after test completion.

The default is rollback. This behavior can be changed, however, by putting @Commit or @Rollback at the class or method level.

If you need more control over your transactions, you can use the static methods in TestTransaction. They allow you to start and end transactions, flag them for commit or rollback or check the transaction status.

Beware: Only Test-Managed Transactions Are Rolled Back

Transactions are not rolled back when the code that is invoked does not interact with the database. An example would be writing an end-to-end test that uses RestTemplate to make an HTTP request to some endpoint that then makes a modification in the database. Since the RestTemplate does not interact with the database (it just creates and sends an HTTP request), @Transactional will not rollback anything that is an effect of that HTTP request. This is because a separate transaction, one not controlled by the test, will be started.

Set Up or Tear Down Outside of a Transaction

You can annotate a public void method with @BeforeTransaction or @AfterTransaction. This indicates to Spring that this method should be run before or after a test method is run within a transaction.

Versions

I wrote this code with Spring Boot version 2.2.5.

Leave a Reply

Your email address will not be published. Required fields are marked *