How to rollback nested transactions with Propagation.REQUIRES_NEW in integration tests
Asked Answered
D

3

22

I have several integration tests for various services that extend the following baseclass:

@ContextConfiguration(locations="classpath:applicationContext-test.xml")
@TransactionConfiguration(transactionManager="txManager", defaultRollback=true)
@Transactional
public abstract class IntegrationTestBase extends AbstractTransactionalJUnit4SpringContextTests
{
    //Some setup, filling test data to a HSQLDB-database etc
}

For most cases this works fine, but I have a service class which has transactions defined with propagation=Propagation.REQUIRES_NEW. It seems these transactions are not rolled back (because they are nested transactions and apparently commit within the "outer" transaction?). The "outer" (test-case level) transaction is rolled back, at least according to test logs. The committed transactions mess up some later tests, because they have changed the test data.

I can get around this by forcing the test to re-create and re-populate the database between tests, but my question is, is this expected behavior or am I doing something wrong in my tests? Can the nested transaction be forced to rollback from the testing code?

Diatomite answered 3/3, 2011 at 9:16 Comment(1)
there's an Improvement ticket on this jira.springsource.org/browse/SPR-6908Specter
M
13

This is expected behaviour, and is one of the main reasons to use REQUIRES_NEW :

  • be able to rollback the new transaction, but commit the outer one
  • be able to commit the new transaction, but rollback the outer one

re-populate the database between tests is probably the best solution, and I would use this solution for all the tests: this allows tests to check that everything works correctly, including the commit (which could fail due to flushing, deferred constraints, etc.).

But it you really want to rollback the transaction, a solution would be to add a boolean argument rollbackAtTheEnd to your service, and rollback the transaction if this argument is true.

Materials answered 3/3, 2011 at 10:20 Comment(3)
Thank you, I think I'll re-populate the database between tests, even if the tests run longer this way. I hadn't thought that the commits could fail, so that's a pro for going with committing tests too. The service in question is a bit of a special case, because it is used during Spring Security login, and in certain cases JPA-exceptions are thrown from service methods, causing the login to fail as the whole transaction is marked for rollback (this is why I chose to use REQUIRES_NEW, so the exceptions wouldn't prevent logging in, another option might be using noRollbackFor in the service).Diatomite
Hi I got the same problem but I don't know how to repopulate the database or what are impacts of this? would you tell me about this. thanksPhenylamine
I wish there was a better solution because it takes a lot of time to recreate the db for each integration test.Yoheaveho
S
6

I added comment to Spring improvement ticket on this. I'll copy it here too:

I worked around this problem by converting all service methods that were declaratively setup like this

@Transactional(propagation = REQUIRES_NEW)
public Object doSmth() {
  // doSmthThatRequiresNewTx
}

to use TransactionTemplate instead:

private TransactionTemplate transactionTemplate;

public Object doSmth() {
  return transactionTemplate.execute(new TransactionCallback<Object>() {
            @Override
            public Object doInTransaction(TransactionStatus status) {
                // doSmthThatRequiresNewTx
            }
        });
  }

Under tests I configure transactionTemplate's propagation behavior to be PROPAGATION_REQUIRED, under real app I configure transactionTemplate's propagation behaviour to be PROPAGATION_REQUIRES_NEW. It works as expected. The limitation of this workaround is that under tests it is not possible to assert the fact that inner transaction is not rolledback in an exceptional scenario.

The other solution would be to explicitly delete everything doSmth() does in the database in the @AfterTransaction method in test. That 'delete' SQL will be run in the new transaction, as its results would be otherwise rolled back routinely by Spring's TransactionConfiguration default behaviour.

Specter answered 11/11, 2011 at 0:57 Comment(0)
B
0

What you could do at the end of each tests is to restore the state of all the created/updated/deleted entities and calling this code :

TestTransaction.flagForCommit();
TestTransaction.end();

I'm not such a big fan of this solution, but for small unitary tests that require transactions, it can be acceptable.

And to go further, here is complete exemple of what I do :


    @Test
    void feedTable_TxN_should_create_entity() {

        int size = repository.findAll().size();

        LineDataDTO line = new LineDataDTO();
        line.setId(myId);
        // Setters

        service.feedTable_TxN(line);

        assertThat(repository.findAll()).hasSize(size + 1);

        Optional<MyEntity> entityOpt = repository.findById(myId);
        assertThat(entityOpt).isPresent();

        MyEntity entity = gaugeOpt.get();

        assertThat(entity.someGetter()).isEqualTo(someValue);

        repository.delete(entity);
        TestTransaction.flagForCommit();
        TestTransaction.end();
    }

If you want to persist an entity that your service needs you have to do this :

repository.save(entity);

TestTransaction.flagForCommit();
TestTransaction.end();
TestTransaction.start();

// call the service with transactional requires new

// Make assertions

repository.delete(entity);
TestTransaction.flagForCommit();
TestTransaction.end();
Brooking answered 25/1, 2022 at 11:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.