Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing @TransactionalEvents and @Rollback

I've been trying to test out @TransactionalEvents (a feature of Spring 4.2 https://spring.io/blog/2015/02/11/better-application-events-in-spring-framework-4-2) with our existing Spring JUnit Tests (run via either @TransactionalTestExecutionListener or subclassing AbstractTransactionalUnit4SpringContextTests but, it seems like there's a forced choice -- either run the test without a @Rollback annotation, or the events don't fire. Has anyone come across a good way to test @TransactionalEvents while being able to @Rollback tests?

like image 991
adam Avatar asked Apr 10 '16 22:04

adam


3 Answers

Stéphane Nicoll is correct: if the TransactionPhase for your @TransactionalEventListener is set to AFTER_COMMIT, then having a transactional test with automatic rollback semantics doesn't make any sense because the event will never get fired.

In other words, there is no way to have an event fired after a transaction is committed if that transaction is never committed.

So if you really want the event to be fired, you have to let the transaction be committed (e.g., by annotating your test method with @Commit). To clean up after the commit, you should be able to use @Sql in isolated mode to execute cleanup scripts after the transaction has committed. For example, something like the following (untested code) might work for you:

@Transactional
@Commit
@Sql(scripts = "/cleanup.sql", executionPhase = AFTER_TEST_METHOD,
     config = @SqlConfig(transactionMode = TransactionMode.ISOLATED))
@Test
public void test() { /* ... */ }

Regards,

Sam (author of the Spring TestContext Framework)

like image 134
Sam Brannen Avatar answered Oct 23 '22 12:10

Sam Brannen


Sam Brannen's solution almost works with regard to adam's comment.

Actually the methods annotated with @TransactionalEventListener are called after the test method transaction is committed. This is so because the calling method, which raises the event, is executing within a logical transaction, not a physical one.

Instead, when the calling method is executed within a new physical transaction, then the methods annotated with @TransactionalEventListener are invoked at the right time, i.e., before the test method transaction is committed.

Also, we don't need @Commit on the test methods, since we actually don't care about these transactions. However, we do need the @Sql(...) statement as explained by Sam Brannen to undo the committed changes of the calling method.

See the small example below.

First the listener that is called when the transaction is committed (default behavior of @TransactionalEventListener):

@Component
public class MyListener {

    @TransactionalEventListener
    public void when(MyEvent event) {
        ...
    }
}

Then the application service that publishes the event listened to by the above class. Notice that the transactions are configured to be new physical ones each time a method is invoked (see the Spring Framework doc for more details):

@Service
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class MyApplicationService {

    public void doSomething() {
        // ...
        // publishes an instance of MyEvent
        // ...
    }
}

Finally the test method as proposed by Sam Brannen but without the @Commitannotation which is not needed at this point:

@Transactional
@Sql(scripts = "/cleanup.sql", executionPhase = AFTER_TEST_METHOD,
     config = @SqlConfig(transactionMode = TransactionMode.ISOLATED))
@Test
public void test() { 
    MyApplicationService target = // ...
    target.doSomething();
    // the event is now received by MyListener
    // assertions on the side effects of MyListener
    // ...
}

This way it works like a charm :-)

like image 41
Marco Avatar answered Oct 23 '22 13:10

Marco


Marco's solution works but adding REQUIRES_NEW propagation into business code is not always acceptable. This modifies business process behavior.

So we should assume that we can change only test part.

Solutin 1

@TestComponent // can be used with spring boot
public class TestApplicationService {

    @Autowired
    public MyApplicationService service;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void doSomething() {
        service.doSomething();
   }
}

This wraps the real service into test component that can be decorated with REQUIRES_NEW propagation. This solution doesn't modify other logic than testing.

Solution 2

@Transactional
@Sql(scripts = "/cleanup.sql", executionPhase = AFTER_TEST_METHOD,
     config = @SqlConfig(transactionMode = TransactionMode.ISOLATED))
@Test
public void test() { 
    MyApplicationService target = // ...
    target.doSomething();
    TestTransaction.flagForCommit(); //Spring-test since 4.1 - thx for Sam Brannen
    TestTransaction.end();
    // the event is now received by MyListener
    // assertions on the side effects of MyListener
    // ...
}

This is simplest solution. We can end test transaction and mark it for commit. This forces transactional events to be handled. If test changes data, cleanup sql script must be specified, otherwise we introduce side effects with committed modified data.

like image 28
Raino Kolk Avatar answered Oct 23 '22 12:10

Raino Kolk