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).

Lookahead timeouts and test-driven transactions

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 the TimeOut rules from JUnit 4

  • Methods assertTimeoutPreemptively(...) from JUnit Jupite in class org.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.

Table 1. Support for attributes annotated with @Transactional
Attribute Supported for test-driven transactions

value and transactionManager

yes

propagation

supported only Propagation.NOT_SUPPORTED and Propagation.NEVER

isolation

no

timeout

no

readOnly

no

rollbackFor and rollbackForClassName

no: use TestTransaction.flagForRollback()

noRollbackFor and noRollbackForClassName

no: instead use TestTransaction.flagForCommit()

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:

Java

@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"));
    }
}
Kotlin

@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.

Java

@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"));
    }
}
Kotlin

@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.

Any "before" methods (for example, methods marked with the annotation @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:

Java

@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
    }
}
Kotlin

@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
    }
}
Avoiding false positives when testing ORM code

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:

Java

// ...
@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();
}
// ...
Kotlin

// ...
@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:

Java

// ...
@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();
}
// ...
Kotlin

// ...
@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()
}
// ...
Testing ORM entity lifecycle callbacks

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.

Java

// ...
@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...
}
//...
Kotlin

// ...
@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.