Reputation: 6452
I have a Spring service that does something like that :
@Service
public class MyService {
@Transactional(propagation = Propagation.NEVER)
public void doStuff(UUID id) {
// call an external service, via http for example, can be long
// update the database, with a transactionTemplate for example
}
}
The Propagation.NEVER indicates we must not have an active transaction when the method is called because we don't want to block a connection to the database while waiting for an answer from the external service.
Now, how could I properly test this and then rollback the database ? @Transactional on the test won't work, there will be an exception because of Propagation.NEVER.
@SpringBootTest
@Transactional
public class MyServiceTest {
@Autowired
private MyService myService;
public void testDoStuff() {
putMyTestDataInDb();
myService.doStuff(); // <- fails no transaction should be active
assertThat(myData).isTheWayIExpectedItToBe();
}
}
I can remove the @Transactional but then my database is not in a consistent state for the next test.
For now my solution is to truncate all tables of my database after each test in a @AfterEach junit callback, but this is a bit clunky and gets quite slow when the database has more than a few tables.
Here comes my question : how could I rollback the changes done to my database without truncating/using @Transactional ?
The database I'm testing against is mariadb with testcontainers, so a solution that would work only with mariadb/mysql would be enough for me. But something more general would be great !
(another exemple where I would like to be able to not use @Transactional on the test : sometimes I want to test that transaction boundaries are correctly put in the code, and not hit some lazy loading exceptions at runtime because I forgot a @Transactional somewhere in the production code).
Some other precisions, if that helps :
Others ideas I've played with :
START TRANSACTION
statements natively on the datasource before the test and ROLLBACK
after the tests : really dirty, could not make it workUpvotes: 5
Views: 617
Reputation: 2737
This is bit of wild idea, but if you are using mysql database, then maybe switch to dolt for tests?
Dolt is a SQL database that you can fork, clone, branch, merge, push and pull just like a git repository.
You can wrap it as testcontainers container, load necessary data on start and then, on start of each test run dolt reset.
Upvotes: 1
Reputation: 2238
Personal opinion: @Transactional
+ @SpringBootTest
is (in a way) the same anti-pattern as spring.jpa.open-in-view
. Yes, it's easy to get things working at first and having the automatic rollback is nice, but it loses you a lot of flexibility and control over your transactions. Anything that requires manual transaction management becomes very hard to test that way.
We recently had a very similar case and in the end we decided to bite the bullet and use @DirtiesContext
instead. Yeah, tests take 30 more minutes to run, but as an added benefit the tested services behave the exact same way as in production and the tests are more likely to catch any transaction issues.
But before we did the switch, we considered using the following workaround:
interface TransactionService
{
void runWithoutTransaction(Runnable runnable);
}
@Service
public class RealTransactionService implements TransactionService
{
@Transactional(propagation = Propagation.NEVER)
public void runWithoutTransaction(Runnable runnable)
{
runnable.run();
}
}
#runWithoutTransaction
-Method, e.g.:@Service
public class MyService
{
@Autowired
private TransactionService transactionService;
public void doStuff(UUID id)
{
transactionService.runWithoutTransaction(() -> {
// call an external service
})
}
}
That way your production code will peform the Propagation.NEVER
check, and for the tests you can replace the TransactionService
with a different implemention that doesn't have the @Transactional
annotations, e.g.:
@Service
@Primary
public class FakeTransactionService implements TransactionService
{
// No annotation here
public void runWithoutTransaction(Runnable runnable)
{
runnable.run();
}
}
This is not limited to Propagation.NEVER
. Other propagation types can be implemented in the same way:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void runWithNewTransaction(Runnable runnable)
{
runnable.run();
}
And finally - the Runnable
parameter can be replaced with a Function
/Consumer
/Supplier
if the method needs to return and/or accept a value.
Upvotes: 2