Transaction Management
In the TestContext framework, transactions are managed by the
TransactionalTestExecutionListener
, which is configured by default, even if you do not
explicitly declare @TestExecutionListeners
on your test class. To enable support for
transactions, however, you must configure a PlatformTransactionManager
bean in the
ApplicationContext
that is loaded with @ContextConfiguration
semantics (further
details are provided later). In addition, you must declare Spring’s @Transactional
annotation either at the class or the method level for your tests.
Test-managed Transactions
Test-managed transactions are transactions that are managed declaratively by using the
TransactionalTestExecutionListener
or programmatically by using TestTransaction
(described later). You should not confuse such transactions with Spring-managed
transactions (those managed directly by Spring within the ApplicationContext
loaded for
tests) or application-managed transactions (those managed programmatically within
application code that is invoked by tests). Spring-managed and application-managed
transactions typically participate in test-managed transactions. However, you should use
caution if Spring-managed or application-managed transactions are configured with any
propagation type other than REQUIRED
or SUPPORTS
(see the discussion on
transaction propagation for details).
Preemptive timeouts and test-managed transactions
Caution must be taken when using any form of preemptive timeouts from a testing framework in conjunction with Spring’s test-managed transactions. Specifically, Spring’s testing support binds transaction state to the current thread (via
a Situations in which this can occur include but are not limited to the following.
|
Enabling and Disabling Transactions
Annotating a test method with @Transactional
causes the test to be run within a
transaction that is, by default, automatically rolled back after completion of the test.
If a test class is annotated with @Transactional
, each test method within that class
hierarchy runs within a transaction. Test methods that are not annotated with
@Transactional
(at the class or method level) are not run within a transaction. Note
that @Transactional
is not supported on test lifecycle methods — for example, methods
annotated with JUnit Jupiter’s @BeforeAll
, @BeforeEach
, etc. Furthermore, tests that
are annotated with @Transactional
but have the propagation
attribute set to
NOT_SUPPORTED
or NEVER
are not run within a transaction.
Attribute | Supported for test-managed transactions |
---|---|
|
yes |
|
only |
|
no |
|
no |
|
no |
|
no: use |
|
no: use |
Method-level lifecycle methods — for example, methods annotated with JUnit Jupiter’s
If you need to run code in a suite-level or class-level lifecycle method within a
transaction, you may wish to inject a corresponding |
Note that AbstractTransactionalJUnit4SpringContextTests
and
AbstractTransactionalTestNGSpringContextTests
are preconfigured for transactional support at the class level.
The following example demonstrates a common scenario for writing an integration test for
a Hibernate-based UserRepository
:
-
Java
-
Kotlin
@SpringJUnitConfig(TestConfig.class)
@Transactional
class HibernateUserRepositoryTests {
@Autowired
HibernateUserRepository repository;
@Autowired
SessionFactory sessionFactory;
JdbcTemplate jdbcTemplate;
@Autowired
void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Test
void createUser() {
// track initial state in test database:
final int count = countRowsInTable("user");
User user = new User(...);
repository.save(user);
// Manual flush is required to avoid false positive in test
sessionFactory.getCurrentSession().flush();
assertNumUsers(count + 1);
}
private int countRowsInTable(String tableName) {
return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
}
private void assertNumUsers(int expected) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
}
}
@SpringJUnitConfig(TestConfig::class)
@Transactional
class HibernateUserRepositoryTests {
@Autowired
lateinit var repository: HibernateUserRepository
@Autowired
lateinit var sessionFactory: SessionFactory
lateinit var jdbcTemplate: JdbcTemplate
@Autowired
fun setDataSource(dataSource: DataSource) {
this.jdbcTemplate = JdbcTemplate(dataSource)
}
@Test
fun createUser() {
// track initial state in test database:
val count = countRowsInTable("user")
val user = User()
repository.save(user)
// Manual flush is required to avoid false positive in test
sessionFactory.getCurrentSession().flush()
assertNumUsers(count + 1)
}
private fun countRowsInTable(tableName: String): Int {
return JdbcTestUtils.countRowsInTable(jdbcTemplate, tableName)
}
private fun assertNumUsers(expected: Int) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"))
}
}
As explained in Transaction Rollback and Commit Behavior, there is no need to
clean up the database after the createUser()
method runs, since any changes made to the
database are automatically rolled back by the TransactionalTestExecutionListener
.
Transaction Rollback and Commit Behavior
By default, test transactions will be automatically rolled back after completion of the
test; however, transactional commit and rollback behavior can be configured declaratively
via the @Commit
and @Rollback
annotations. See the corresponding entries in the
annotation support section for further details.
Programmatic Transaction Management
You can interact with test-managed transactions programmatically by using the static
methods in TestTransaction
. For example, you can use TestTransaction
within test
methods, before methods, and after methods to start or end the current test-managed
transaction or to configure the current test-managed transaction for rollback or commit.
Support for TestTransaction
is automatically available whenever the
TransactionalTestExecutionListener
is enabled.
The following example demonstrates some of the features of TestTransaction
. See the
javadoc for TestTransaction
for further details.
-
Java
-
Kotlin
@ContextConfiguration(classes = TestConfig.class)
public class ProgrammaticTransactionManagementTests extends
AbstractTransactionalJUnit4SpringContextTests {
@Test
public void transactionalTest() {
// assert initial state in test database:
assertNumUsers(2);
deleteFromTables("user");
// changes to the database will be committed!
TestTransaction.flagForCommit();
TestTransaction.end();
assertFalse(TestTransaction.isActive());
assertNumUsers(0);
TestTransaction.start();
// perform other actions against the database that will
// be automatically rolled back after the test completes...
}
protected void assertNumUsers(int expected) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
}
}
@ContextConfiguration(classes = [TestConfig::class])
class ProgrammaticTransactionManagementTests : AbstractTransactionalJUnit4SpringContextTests() {
@Test
fun transactionalTest() {
// assert initial state in test database:
assertNumUsers(2)
deleteFromTables("user")
// changes to the database will be committed!
TestTransaction.flagForCommit()
TestTransaction.end()
assertFalse(TestTransaction.isActive())
assertNumUsers(0)
TestTransaction.start()
// perform other actions against the database that will
// be automatically rolled back after the test completes...
}
protected fun assertNumUsers(expected: Int) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"))
}
}
Running Code Outside of a Transaction
Occasionally, you may need to run certain code before or after a transactional test
method but outside the transactional context — for example, to verify the initial
database state prior to running your test or to verify expected transactional commit
behavior after your test runs (if the test was configured to commit the transaction).
TransactionalTestExecutionListener
supports the @BeforeTransaction
and
@AfterTransaction
annotations for exactly such scenarios. You can annotate any void
method in a test class or any void
default method in a test interface with one of these
annotations, and the TransactionalTestExecutionListener
ensures that your before
transaction method or after transaction method runs at the appropriate time.
Any before methods (such as methods annotated with JUnit Jupiter’s @BeforeEach )
and any after methods (such as methods annotated with JUnit Jupiter’s @AfterEach ) are
run within a transaction. In addition, methods annotated with @BeforeTransaction or
@AfterTransaction are not run for test methods that are not configured to run within a
transaction.
|
Configuring a Transaction Manager
TransactionalTestExecutionListener
expects a PlatformTransactionManager
bean to be
defined in the Spring ApplicationContext
for the test. If there are multiple instances
of PlatformTransactionManager
within the test’s ApplicationContext
, you can declare a
qualifier by using @Transactional("myTxMgr")
or @Transactional(transactionManager =
"myTxMgr")
, or TransactionManagementConfigurer
can be implemented by an
@Configuration
class. Consult the
javadoc
for TestContextTransactionUtils.retrieveTransactionManager()
for details on the
algorithm used to look up a transaction manager in the test’s ApplicationContext
.
Demonstration of All Transaction-related Annotations
The following JUnit Jupiter based example displays a fictitious integration testing
scenario that highlights all transaction-related annotations. The example is not intended
to demonstrate best practices but rather to demonstrate how these annotations can be
used. See the annotation support section for further
information and configuration examples. Transaction management for @Sql
contains an additional example that uses @Sql
for
declarative SQL script execution with default transaction rollback semantics. The
following example shows the relevant annotations:
-
Java
-
Kotlin
@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {
@BeforeTransaction
void verifyInitialDatabaseState() {
// logic to verify the initial state before a transaction is started
}
@BeforeEach
void setUpTestDataWithinTransaction() {
// set up test data within the transaction
}
@Test
// overrides the class-level @Commit setting
@Rollback
void modifyDatabaseWithinTransaction() {
// logic which uses the test data and modifies database state
}
@AfterEach
void tearDownWithinTransaction() {
// run "tear down" logic within the transaction
}
@AfterTransaction
void verifyFinalDatabaseState() {
// logic to verify the final state after transaction has rolled back
}
}
@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {
@BeforeTransaction
fun verifyInitialDatabaseState() {
// logic to verify the initial state before a transaction is started
}
@BeforeEach
fun setUpTestDataWithinTransaction() {
// set up test data within the transaction
}
@Test
// overrides the class-level @Commit setting
@Rollback
fun modifyDatabaseWithinTransaction() {
// logic which uses the test data and modifies database state
}
@AfterEach
fun tearDownWithinTransaction() {
// run "tear down" logic within the transaction
}
@AfterTransaction
fun verifyFinalDatabaseState() {
// logic to verify the final state after transaction has rolled back
}
}
Avoid false positives when testing ORM code
When you test application code that manipulates the state of a Hibernate session or JPA persistence context, make sure to flush the underlying unit of work within test methods that run that code. Failing to flush the underlying unit of work can produce false positives: Your test passes, but the same code throws an exception in a live, production environment. Note that this applies to any ORM framework that maintains an in-memory unit of work. In the following Hibernate-based example test case, one method demonstrates a false positive, and the other method correctly exposes the results of flushing the session:
The following example shows matching methods for JPA:
|
Testing ORM entity lifecycle callbacks
Similar to the note about avoiding false positives when testing ORM code, if your application makes use of entity lifecycle callbacks (also known as entity listeners), make sure to flush the underlying unit of work within test methods that run that code. Failing to flush or clear the underlying unit of work can result in certain lifecycle callbacks not being invoked. For example, when using JPA, The following example shows how to flush the
See JpaEntityListenerTests in the Spring Framework test suite for working examples using all JPA lifecycle callbacks. |