Rollback changes done to a MariaDB database by a spring test without @Transactional

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 :

Upvotes: 5

Views: 617

Answers (2)

Bartek Jablonski
Bartek Jablonski

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

Bragolgirith
Bragolgirith

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:

  1. Create an interface and a service similar to the following:
interface TransactionService
{

    void runWithoutTransaction(Runnable runnable);

}
@Service
public class RealTransactionService implements TransactionService
{

    @Transactional(propagation = Propagation.NEVER)
    public void runWithoutTransaction(Runnable runnable)
    {
        runnable.run();
    }

}
  1. In your other service wrap the external http calls with the #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

Related Questions