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
@Transactional
Do? - 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
REQUIRED
orSUPPORTS
. — 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.