Automatic Rollback of Transactions in Spring Tests
Put
@Transactional
on your test class or methods and test-managed transactions will
automatically be rollbacked.
Table of Contents
- The Antipattern
- Difference Between Test-, Spring- and Application-Managed Transactions
- What Does
@TransactionalDo? - Beware: Only Test-Managed Transactions Are Rolled Back
- Set Up or Tear Down Outside of a Transaction
- Spring Boot Version
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
REQUIREDorSUPPORTS. — Documentation forTransactionalTestExecutionListener
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.
Spring Boot Version
I wrote this code with Spring Boot version 2.2.5.