1. Introduction
Spring has excellent support for declarative transaction management throughout application code as well as in integration tests.
However, we may occasionally need fine-grained control over transaction boundaries.
In this article, we’ll see how to programmatically interact with automatic transactions set up by Spring in transactional tests.
2. Prerequisites
Let’s assume that we have some integration tests in our Spring application.
Specifically, we’re considering tests that interact with a database, for example, check that our persistence layer is behaving correctly.
Let’s consider a standard test class – annotated as transactional:
@Transactional
@ContextConfiguration(classes = { HibernateXMLConf.class })
@ExtendWith(SpringExtension.class)
class HibernateBootstrapIntegrationTest {
In such a test, every test method is wrapped in a transaction, which gets rolled back when the method exits.
It’s, of course, also possible to only annotate specific methods. Everything we’ll discuss in this article applies to that scenario as well.
3. The TestTransaction Class
We’ll spend the rest of the article discussing a single class: org.springframework.test.context.transaction.TestTransaction.
This is a utility class with a few static methods that we can use to interact with transactions in our tests.
Each method interacts with the only current transaction which is in place during the execution of a test method.
3.1. Checking the State of the Current Transaction
One thing we often do in tests is checking that things are in the state they are supposed to be.
Therefore, we might want to check whether there is a currently active transaction:
assertTrue(TestTransaction.isActive());
Or, we could be interested to check whether the current transaction is flagged for rollback or not:
assertTrue(TestTransaction.isFlaggedForRollback());
If it is, then Spring will roll it back just before it ends, either automatically or programmatically. Otherwise, it’ll commit it just before closing it.
3.2. Flagging a Transaction for Commit or Rollback
We can change programmatically the policy to commit or to rollback the transaction before closing it:
TestTransaction.flagForCommit();
TestTransaction.flagForRollback();
Normally, transactions in tests are flagged for rollback when they start. However, if the method has a @Commit annotation, they start flagged for commit instead:
@Test
@Commit
public void testFlagForCommit() {
assertFalse(TestTransaction.isFlaggedForRollback());
}
Note that these methods merely flag the transaction, as their names imply. That is, the transaction isn’t committed or rolled back immediately, but only just before it ends.
3.3. Starting and Ending a Transaction
To commit or rollback a transaction, we either let the method exit, or we explicitly end it:
TestTransaction.end();
If later on, we want to interact with the database again, we have to start a new transaction:
TestTransaction.start();
Note that the new transaction will be flagged for rollback (or commit) as per the method’s default. In other words, previous calls to flagFor… don’t have any effect on new transactions.
4. Some Implementation Details
TestTransaction is nothing magical. We’ll now look at its implementation to learn a little more about transactions in tests with Spring.
We can see that its few methods simply get access to the current transaction and encapsulate some of its functionality.
4.1. Where Does TestTransaction Get the Current Transaction From?
Let’s go straight to the code:
TransactionContext transactionContext
= TransactionContextHolder.getCurrentTransactionContext();
TransactionContextHolder is just a static wrapper around a ThreadLocal holding a TransactionContext.
4.2. Who Sets the Thread-Local Context?
If we look at who calls the setCurrentTransactionContext method, we’ll find there’s only one caller: TransactionalTestExecutionListener.beforeTestMethod.
TransactionalTestExecutionListener is the listener that Springs configures automatically on tests that are annotated @Transactional.
Note that TransactionContext doesn’t hold a reference to any actual transaction; instead, it is merely a façade over the PlatformTransactionManager.
Yes, this code is heavily layered and abstract. Such are, often, the core parts of the Spring framework.
It’s interesting to see how, under the complexity, Spring doesn’t do any black magic – just a lot of necessary bookkeeping, plumbing, exception handling and so on.
5. Conclusions
In this quick tutorial, we’ve seen how to interact programmatically with transactions in Spring-based tests.
The implementation of all these examples can be found in the GitHub project – this is a Maven project, so it should be easy to import and run as it is.