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?
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)
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 @Commit
annotation 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 :-)
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.
@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.
@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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With