GH-150 - Scenario now execute stimulus within new transaction.

To make sure that we trigger transactional event listeners from Scenario based integration tests, we trigger stimulus executions using transactions configured to require a new transaction. This makes sure that Scenarios work in integration tests using @Transactional, of course circumventing the default rollback behavior of those transactions. There's no other way to implement this as we *need* to commit a transaction to trigger the delivery of transaction-bound events.

Fixed a bug in the handling of Scenario.stimulus(Supplier<T>) as for this, the Supplier handed into the method had not been executed transactionally at all.
This commit is contained in:
Oliver Drotbohm
2023-02-26 20:26:53 +01:00
parent 47a8a7fa77
commit ce264aaf96
4 changed files with 45 additions and 30 deletions

View File

@@ -34,7 +34,10 @@ import org.springframework.modulith.test.PublishedEvents.TypedPublishedEvents;
import org.springframework.modulith.test.PublishedEventsAssert.PublishedEventAssert;
import org.springframework.modulith.test.Scenario.When.EventResult;
import org.springframework.modulith.test.Scenario.When.StateChangeResult;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionOperations;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.Assert;
import com.tngtech.archunit.thirdparty.com.google.common.base.Optional;
@@ -72,21 +75,24 @@ public class Scenario {
private final AssertablePublishedEvents events;
/**
* Creates a new {@link Scenario} for the given {@link TransactionOperations}, {@link ApplicationEventPublisher} and
* Creates a new {@link Scenario} for the given {@link TransactionTemplate}, {@link ApplicationEventPublisher} and
* {@link AssertablePublishedEvents}.
*
* @param transactionOperations must not be {@literal null}.
* @param transactionTemplate must not be {@literal null}.
* @param publisher must not be {@literal null}.
* @param events must not be {@literal null}.
*/
Scenario(TransactionOperations transactionOperations, ApplicationEventPublisher publisher,
Scenario(TransactionTemplate transactionTemplate, ApplicationEventPublisher publisher,
AssertablePublishedEvents events) {
Assert.notNull(transactionOperations, "TransactionOperations must not be null!");
Assert.notNull(transactionTemplate, "TransactionTemplate must not be null!");
Assert.notNull(publisher, "ApplicationEventPublisher must not be null!");
Assert.notNull(events, "AssertablePublishedEvents must not be null!");
this.transactionOperations = transactionOperations;
var definition = new DefaultTransactionDefinition(transactionTemplate);
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
this.transactionOperations = new TransactionTemplate(transactionTemplate.getTransactionManager(), definition);
this.publisher = publisher;
this.events = events;
}
@@ -145,7 +151,7 @@ public class Scenario {
* @see EventResult#toArriveAndVerify(Consumer)
*/
public <S> When<S> stimulate(Supplier<S> supplier) {
return stimulate(__ -> supplier.get());
return stimulate(tx -> tx.execute(__ -> supplier.get()));
}
/**

View File

@@ -21,7 +21,7 @@ import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.support.TransactionOperations;
import org.springframework.transaction.support.TransactionTemplate;
/**
* JUnit {@link ParameterResolver} for {@link Scenario}.
@@ -70,7 +70,7 @@ class ScenarioParameterResolver implements ParameterResolver, BeforeAllCallback
throws ParameterResolutionException {
var context = SpringExtension.getApplicationContext(extensionContext);
var operations = context.getBean(TransactionOperations.class);
var operations = context.getBean(TransactionTemplate.class);
var events = (AssertablePublishedEvents) delegate.resolveParameter(parameterContext, extensionContext);
return new Scenario(operations, context, events);

View File

@@ -27,6 +27,7 @@ import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
@@ -35,10 +36,9 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.PayloadApplicationEvent;
import org.springframework.modulith.test.PublishedEventsAssert.PublishedEventAssert;
import org.springframework.modulith.test.Scenario.When;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.support.SimpleTransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionOperations;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
/**
* Unit tests for {@link Scenario}.
@@ -52,8 +52,15 @@ class ScenarioUnitTests {
private static final Duration WAIT_TIME = Duration.ofMillis(101);
private static final Duration TIMED_OUT = Duration.ofMillis(150);
TransactionTemplate tx;
@Mock ApplicationEventPublisher publisher;
TransactionOperations tx = StubTransactionOperations.INSTANCE;
@Mock PlatformTransactionManager txManager;
@BeforeEach
void setUp() {
this.tx = new TransactionTemplate(txManager);
}
@Test // GH-136
void timesOutIfNoEventArrivesInTime() throws Throwable {
@@ -328,6 +335,18 @@ class ScenarioUnitTests {
verify(verification, never()).accept(any(), any());
}
@Test // GH-150
void executesStimulusInNewTransaction() {
Consumer<Scenario> scenario = it -> it.stimulate(() -> 41)
.andWaitForStateChange(() -> true);
givenAScenario(scenario).expectSuccess();
verify(txManager)
.getTransaction(argThat(it -> it.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW));
}
private Fixture givenAScenario(Consumer<Scenario> consumer) {
return new Fixture(consumer, DELAY, null, new DefaultAssertablePublishedEvents());
}
@@ -377,21 +396,21 @@ class ScenarioUnitTests {
public void expectSuccess() {
foo();
execute();
exceptionHandler.throwIfCaught();
}
public void expectFailure() {
foo();
execute();
assertThat(exceptionHandler.caught)
.as("Expected failure but did not see an exception!")
.isNotNull();
}
private void foo() {
private void execute() {
try {
@@ -422,20 +441,6 @@ class ScenarioUnitTests {
record SomeEvent(String payload) {}
enum StubTransactionOperations implements TransactionOperations {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.transaction.support.TransactionOperations#execute(org.springframework.transaction.support.TransactionCallback)
*/
@Override
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
return action.doInTransaction(new SimpleTransactionStatus());
}
}
static class CapturingExceptionHandler implements UncaughtExceptionHandler {
Throwable caught;

View File

@@ -120,6 +120,10 @@ scenario.stimulate(() -> someBean.someMethod(…)).…
----
Both the event publication and bean invocation will happen within a transaction callback to make sure the given event or any ones published during the bean invocation will be delivered to transactional event listeners.
Note, that this will require a *new* transaction to be started, no matter whether the test case is already running inside a transaction or not.
In other words, state changes of the database triggered by the stimulus will *never* be rolled back and have to be cleaned up manually.
See the `….andCleanup(…)` methods for that purpose.
The resulting object can now get the execution customized though the generic `….customize(…)` method or specialized ones for common use cases like setting a timeout (`….waitAtMost(…)`).
The setup phase will be concluded by defining the actual expectation of the outcome of the stimulus.