In the TestContext framework, transactions are managed using the TransactionalTestExecutionListener
, which is configured by default, even if the @TestExecutionListeners
annotation is not explicitly declared in its test class. However, to enable transaction support, you must configure a PlatformTransactionManager
bean in the ApplicationContext
, which is loaded with the semantics of the @ContextConfiguration
annotation (more details will be provided later ). Additionally, you need to declare the @Transactional
annotation from Spring for your tests, either at the class or method level.
Test Driven Transactions
Test-driven transactions are transactions that are controlled declaratively using TransactionalTestExecutionListener
or programmatically using TestTransaction
(described later). Such transactions should not be confused with Spring-managed transactions (managed directly through the Spring framework in the ApplicationContext
loaded for tests) or application-managed transactions (managed programmatically in the application code that is called by the tests). Spring-managed and application-managed transactions typically involve test-driven transactions. However, caution should be exercised if Spring or application-managed transactions are configured using any propagation type other than REQUIRED
or SUPPORTS
(see the subsection for transaction propagation details).
Caution must be exercised when using any form of lookahead timeout from a testing framework in combination with Spring test-driven transactions.
Specifically, Spring's testing support binds transaction state to the current thread (via the java.lang.ThreadLocal
variable) before calling the current test method. If the test framework calls the current test method on a new thread to support a preemptive timeout, then any actions performed in the current test method will not be called in the test-driven transaction. Therefore, the result of any such actions cannot be rolled back in a test-managed transaction. Instead, such actions will be committed to persistent storage - such as a relational database - even if the test-driven transaction is properly rolled back by Spring.
Cases in which this may occur may include, but are not limited to, those as shown below.
Supports the
@Test(timeout = ...)
annotation and theTimeOut
rules from JUnit 4Methods
assertTimeoutPreemptively(...)
from JUnit Jupite in classorg.junit.jupiter.api.Assertions
Support for the
@Test(timeOut = ...)
annotation from TestNG
Activation and deactivation of transactions
Annotating a test method with @Transactional
causes the test to run in a transaction, which by default is automatically rolled back when the test completes. If a test class is marked with the @Transactional
annotation, then each test method in the class hierarchy is executed within a transaction. Test methods that are not annotated with @Transactional
(at the class or method level) are not executed within a transaction. Please note that the @Transactiona.l
annotation is not supported for test lifecycle methods - for example, methods annotated with the @BeforeAll
, @BeforeEach
annotations from JUnit Jupiter, etc. Moreover, tests marked with the @Transactional
annotation but having the propagation
attribute set to NOT_SUPPORTED
or NEVER
are not are executed as part of a transaction.
Attribute | Supported for test-driven transactions |
---|---|
|
yes |
|
supported only |
|
no |
|
no |
|
no |
|
no: use |
|
no: instead use |
Method-level lifecycle methods—for example, methods annotated with @BeforeEach
or @AfterEach
from JUnit Jupiter - run within a test-driven transaction. On the other hand, lifecycle methods at the suite and class level - for example, methods annotated with @BeforeAll
or @AfterAll
from JUnit Jupiter and methods annotated with @BeforeSuite
, @AfterSuite
, @BeforeClass
or @AfterClass
from TestNG - ne are executed within a test-driven framework transactions.
If you want to run code in a bundle-level or class-level lifecycle method within a transaction, you can inject the appropriate PlatformTransactionManager
into a test class and then use it with TransactionTemplate
for programmatic transaction management.
Note that AbstractTransactionalJUnit4SpringContextTests
and AbstractTransactionalTestNGSpringContextTests
are preconfigured to support class-level transactions.
The following example demonstrates a common scenario for writing an integration test for a UserRepository
based on Hibernate:
@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() {
// keep track of the initial state in the test database:
final int count = countRowsInTable("user");
User user = new User(...);
repository.save(user);
// To avoid false positives during testing, a manual reset is required
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() {
// keep track of the initial state in test database :
val count = countRowsInTable("user")
val user = User()
repository.save(user)
// To avoid false positives during testing, a manual reset is required
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 the section "Transaction Rollback and Commit Logic", there is no need to clean up the database after executing the createUser()
method, since any changes made to the database are automatically rolled back by TransactionalTestExecutionListener
.
Logic for rolling back and committing transactions
By default, test transactions are automatically rolled back after the test completes; however, the logic for committing and rolling back transactions can be configured declaratively using the @Commit
and @Rollback
annotations. For more information, see the related entries in the section on Annotation Support.
Program Transaction Management
You can interact with test-driven transactions programmatically using static methods in TestTransaction
. For example, you can use TestTransaction
in test, before, and after methods to start or end the current test-driven transaction, or to configure the current test-driven transaction to rollback or commit. TestTransaction
support is automatically available whenever the TransactionalTestExecutionListener
listener is enabled.
The following example demonstrates some of the capabilities of TestTransaction
. For more information, see the javadoc at TestTransaction
.
@ContextConfiguration(classes = TestConfig.class)
public class ProgrammaticTransactionManagementTests extends
AbstractTransactionalJUnit4SpringContextTests {
@Test
public void transactionalTest() {
// assert the initial state in the test database:
assertNumUsers(2);
deleteFromTables("user");
// changes in the database will be committed!
TestTransaction.flagForCommit();
TestTransaction.end();
assertFalse(TestTransaction.isActive());
assertNumUsers(0);
TestTransaction.start();
// perform other actions with the database, which will
// be automatically rolled back after the test is completed...
}
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() {
// confirm the initial state in the test database:
assertNumUsers(2)
deleteFromTables("user")
// changes in the database will be committed!
TestTransaction.flagForCommit()
TestTransaction.end()
assertFalse(TestTransaction.isActive())
assertNumUsers(0) TestTransaction.start()
// perform other actions with the database, which will
// be automatically rolled back after the test is completed...
}
protected fun assertNumUsers(expected: Int) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"))
}
}
Executing code outside of a transaction
Sometimes you may want to execute certain code before or after a transactional test method, but outside the transactional context - for example, to check the initial state of the database before running a test, or to verify the expected behavior of a transactional test method. commits after test execution (if the test was configured to commit a transaction). TransactionalTestExecutionListener
supports the @BeforeTransaction
and @AfterTransactio.n
annotations for just such scenarios. You can annotate any void
method in a test class or any default void
method in a test interface with one of these annotations, and the TransactionalTestExecutionListener
listener will ensure that your "pre-transaction" or "post-transaction" method was run at the correct time.
@BeforeEach
from JUnit Jupiter) and any "after" methods (for example, methods marked with the
@AfterEach
annotation from JUnit Jupiter) are executed within a transaction. Additionally, methods annotated with the
@BeforeTransaction
or
@AfterTransaction
annotation are not executed for test methods that are not configured to execute within a transaction.
Setting up a transaction manager
TransactionalTestExecutionListener
expects a PlatformTransactionManager
bean to be defined in the ApplicationContext
from Spring for the test. If there are multiple instances of PlatformTransactionManager
within an ApplicationContext
test, you can declare a qualifier using the @Transactional("myTxMgr")
or @Transactional (transactionManager = "myTxMgr")
annotation, or TransactionManagementConfigurer
can be implemented by a class marked with the @Configuration
annotation. Refer to javadoc on TestContextTransactionUtils.retrieveTransactionManager()
for more details about the algorithm used for the search transaction manager in the ApplicationContext
of the test.
Showing all transaction-related annotations
The following JUnit Jupiter-based example shows a fictitious integration testing scenario that highlights all annotations associated with transactions. The example is not intended to demonstrate best practice, but rather serves as a demonstration of how these annotations can be used. For more information and configuration examples, see the Annotation Support section. Transaction Management for the @Sql
annotation contains an additional example using the @Sql
annotation to execute a declarative SQL script with default transaction rollback semantics. The following example shows the corresponding annotations:
@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {
@BeforeTransaction
void verifyInitialDatabaseState() {
// logic for checking the validity of the initial state before starting the transaction
}
@BeforeEach
void setUpTestDataWithinTransaction() {
// set the test data within the transaction
}
@Test
// overrides the @Commit annotation setting at the class level
@Rollback
void modifyDatabaseWithinTransaction() {
// logic using test data and changing the state of the database
}
@AfterEach
void tearDownWithinTransaction() {
// run tear down logic inside the transaction
}
@AfterTransaction
void verifyFinalDatabaseState() {
// logic for checking the validity of the final state after the transaction is rolled back
}
}
@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {
@BeforeTransaction
fun verifyInitialDatabaseState( ) {
// logic for checking the validity of the initial state before starting the transaction
}
@BeforeEach
fun setUpTestDataWithinTransaction() {
// set the test data within the transaction }
@Test
// overrides the @Commit annotation setting at the class level
@Rollback
fun modifyDatabaseWithinTransaction() {
// logic that uses test data and changes the state of the database
}
@AfterEach
fun tearDownWithinTransaction() {
// run tear down logic inside the transaction
}
@AfterTransaction
fun verifyFinalDatabaseState() {
// logic to verify the validity of the final state after the transaction is rolled back
}
}
When you test application code that manipulates the state of a Hibernate session or JPA persistence context, be sure to reset the basic unit of work in the test methods that execute this code. Not resetting the basic unit of work can result in false positives: Your test will pass, but in a real production environment the same code will throw an exception. Note that this applies to any ORM system that stores a unit of work in memory. In the following Hibernate-based test case, one method exhibits a false positive, while another method correctly opens the session reset results:
// ...
@Autowired
SessionFactory sessionFactory;
@Transactional
@Test // no expected exception!
public void falsePositive() {
updateEntityInHibernateSession();
// False positive: an exception will be thrown as soon as the Hibernate session
// is finally flushed (ie in production code)
}
@Transactional
@Test(expected = ...)
public void updateWithSessionFlush() {
updateEntityInHibernateSession();
// To avoid false positives during testing, a manual reset is required
sessionFactory.getCurrentSession().flush();
}
// ...
// ...
@Autowired
lateinit var sessionFactory: SessionFactory
@Transactional
@Test // no expected exception!
fun falsePositive() {
updateEntityInHibernateSession()
// False positive: an exception will be thrown as soon as the Hibernate
// session is finally flushed (ie in production code)
}
@Transactional
@Test(expected = ...)
fun updateWithSessionFlush( ) {
updateEntityInHibernateSession()
// To avoid false positives during testing, a manual reset is required
sessionFactory.getCurrentSession().flush()
}
// ...
The following example shows mapping methods for JPA:
// ...
@PersistenceContext
EntityManager entityManager;
@Transactional
@Test // no expected exception!
public void falsePositive() {
updateEntityInJpaPersistenceContext();
// False Positive: An exception will be thrown as soon as
// the EntityManager from JPA is finally flushed (ie in production code)
}
@Transactional
@Test(expected = ...)
public void updateWithEntityManagerFlush() {
updateEntityInJpaPersistenceContext();
// To avoid false positives during testing, a manual reset is required
entityManager.flush();
}
// ...
// ...
@PersistenceContext
lateinit var entityManager:EntityManager
@Transactional
@Test // no exception expected!
fun falsePositive() {
updateEntityInJpaPersistenceContext()
// False positive: an exception will be thrown as soon as
// the EntityManager from JPA is finally flushed (ie in production code)
}
@Transactional
@Test(expected = ...)
void updateWithEntityManagerFlush () {
updateEntityInJpaPersistenceContext()
// To avoid false positives during testing, a manual reset is required
entityManager.flush()
}
// ...
Similar to the note about how to avoid false positives When testing ORM code, if your application uses entity lifecycle callbacks (also known as entity listeners), ensure that the test methods that execute that code reset the underlying unit of work. Failure to reset or clear a basic unit of work may result in certain lifecycle callbacks not being called.
For example, when using JPA callbacks with annotations @PostPersist
, @PreUpdate
and @PostUpdate
will not commit unless the entityManager.flush()
function is called after saving or updating the entity. Likewise, if an entity is already attached to the current unit of work (associated with the current persistence context), attempting to reload the entity will not trigger the @PostLoad
annotation callback unless the entityManager.clear()
function is called before attempting to reload the entity.
The following example shows how to reset the EntityManager
so that callbacks to the @PostPersist
annotation are guaranteed to fail when the entity will be saved. The Person
entity used in the example had an entity listener registered with a callback method marked with the @PostPersist
annotation.
// ...
@Autowired
JpaPersonRepository repo;
@PersistenceContext
EntityManager entityManager;
@Transactional
@Test
void savePerson() {
// EntityManager#persist(...) results in @PrePersist but not @PostPersist
repo.save(new Person("Jane"));
// Callback with @PostPersist annotation requires manual flush
entityManager.flush();
// Test code that uses a callback with the @PostPersist annotation
// was called...
}
//...
// ...
@Autowired
lateinit var repo: JpaPersonRepository
@PersistenceContext
lateinit var entityManager: EntityManager
@Transactional
@Test
fun savePerson() {
// EntityManager#persist(...) results in @PrePersist, but not @PostPersist
repo.save(Person("Jane"))
// The callback with the @PostPersist annotation requires a manual flush
entityManager.flush()
// Test code that uses the callback with the @PostPersist annotation
// was called ...
}
// ...
See JpaEntityListenerTests in the Spring Framework Test Suite for working examples that use all of the JPA lifecycle callbacks.
GO TO FULL VERSION