diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/Scenario.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/Scenario.java index 94d672d8..bca72d62 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/Scenario.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/Scenario.java @@ -16,7 +16,6 @@ package org.springframework.modulith.test; import java.time.Duration; -import java.util.Arrays; import java.util.concurrent.Callable; import java.util.function.BiConsumer; import java.util.function.BiFunction; @@ -55,6 +54,19 @@ import com.tngtech.archunit.thirdparty.com.google.common.base.Optional; @API(status = Status.EXPERIMENTAL) public class Scenario { + private static final Predicate DEFAULT_ACCEPTANCE = it -> { + + if (it instanceof Optional o) { + return o.isPresent(); + } + + if (it instanceof Boolean b) { + return b; + } + + return it != null; + }; + private final TransactionOperations transactionOperations; private final ApplicationEventPublisher publisher; private final AssertablePublishedEvents events; @@ -86,9 +98,9 @@ public class Scenario { * @param event must not be {@literal null}. * @return will never be {@literal null}. */ - public When publish(Object... event) { + public When publish(Object event) { return stimulate((tx, e) -> { - tx.executeWithoutResult(__ -> Arrays.stream(event).forEach(e::publishEvent)); + tx.executeWithoutResult(__ -> e.publishEvent(event)); }); } @@ -99,11 +111,10 @@ public class Scenario { * @param events must not be {@literal null}. * @return will never be {@literal null}. */ - @SuppressWarnings("unchecked") - public When publish(Supplier... events) { + public When publish(Supplier event) { return stimulate((tx, e) -> { - tx.executeWithoutResult(__ -> Arrays.stream(events).map(Supplier::get).forEach(e::publishEvent)); + tx.executeWithoutResult(__ -> e.publishEvent(event.get())); }); } @@ -120,7 +131,7 @@ public class Scenario { return stimulate(() -> { runnable.run(); return null; - }, __ -> {}); + }); } /** @@ -134,24 +145,7 @@ public class Scenario { * @see EventResult#toArriveAndVerify(Consumer) */ public When stimulate(Supplier supplier) { - return stimulate(supplier, __ -> {}); - } - - /** - * Stimulates the system using the given {@link Supplier}, keeping the supplied value around for later verification - * but also registers a cleanup callback to make sure potential state changes are rolled back for the test execution. - * - * @param the type of the value returned by the stimulus. - * @param supplier must not be {@literal null}. - * @param cleanupCallback must not be {@literal null}. - * @return will never be {@literal null}. - */ - public When stimulate(Supplier supplier, Consumer cleanupCallback) { - - Assert.notNull(supplier, "Supplier must not be null!"); - Assert.notNull(cleanupCallback, "Cleanup callback must not be null!"); - - return stimulate((tx) -> tx.execute(status -> supplier.get()), cleanupCallback); + return stimulate(__ -> supplier.get()); } /** @@ -163,25 +157,9 @@ public class Scenario { * @return will never be {@literal null}. */ public When stimulate(Function function) { - return stimulate(function, __ -> {}); - } - - /** - * Stimulates the system using the given function providing access to the {@link TransactionOperations} and keeping - * the supplied value around for later verification but also registers a cleanup callback to make sure potential state - * changes are rolled back for the test execution. - * - * @param the type of the value returned by the stimulus. - * @param function must not be {@literal null}. - * @param cleanupCallback must not be {@literal null}. - * @return will never be {@literal null}. - */ - public When stimulate(Function function, Consumer cleanupCallback) { - - Assert.notNull(function, "Function must not be null!"); - Assert.notNull(cleanupCallback, "Cleanup callback must not be null!"); - - return stimulate((tx, events) -> function.apply(tx), cleanupCallback); + return stimulate((tx, __) -> { + return function.apply(tx); + }); } /** @@ -195,24 +173,10 @@ public class Scenario { Assert.notNull(stimulus, "Stimulus must not be null!"); - return stimulate(stimulus, () -> {}); - } - - /** - * @param stimulus must not be {@literal null}. - * @param cleanupCallback must not be {@literal null}. - * @return will never be {@literal null}. - */ - public When stimulate(BiConsumer stimulus, - Runnable cleanupCallback) { - - Assert.notNull(stimulus, "Stimulus must not be null!"); - Assert.notNull(cleanupCallback, "Cleanup callback must not be null!"); - return stimulate((tx, e) -> { stimulus.accept(tx, e); return (Void) null; - }, __ -> cleanupCallback.run()); + }); } /** @@ -227,26 +191,7 @@ public class Scenario { Assert.notNull(stimulus, "Stimulus must not be null!"); - return stimulate(stimulus, __ -> {}); - } - - /** - * Stimulate the system using the given {@link TransactionOperations} and {@link ApplicationEventPublisher} (usually a - * method on some application service or event publication is triggered), produce a result and a callback to clean up - * after the verification has completed. - * - * @param the type of the result. - * @param stimulus must not be {@literal null}. - * @param cleanupCallback must not be {@literal null}. - * @return will never be {@literal null}. - */ - public When stimulate(BiFunction stimulus, - Consumer cleanupCallback) { - - Assert.notNull(stimulus, "Stimulus must not be null!"); - Assert.notNull(cleanupCallback, "Cleanup callback must not be null!"); - - return new When<>(stimulus, cleanupCallback, Function.identity()); + return new When<>(stimulus, __ -> {}, Function.identity()); } public class When { @@ -267,6 +212,34 @@ public class Scenario { this.customizer = customizer; } + /** + * Registers the given {@link Runnable} as cleanup callback to always run after completion of the {@link Scenario}, + * no matter the outcome of its execution (error or success). + * + * @param runnable must not be {@literal null}. + * @return will never be {@literal null}. + */ + public When andCleanup(Runnable runnable) { + + Assert.notNull(runnable, "Cleanup callback must not be null!"); + + return andCleanup(__ -> runnable.run()); + } + + /** + * Registers the given {@link Consumer} as cleanup callback to always run after completion of the {@link Scenario}, + * no matter the outcome of its execution (error or success). + * + * @param consumer must not be {@literal null}. + * @return will never be {@literal null}. + */ + public When andCleanup(Consumer consumer) { + + Assert.notNull(consumer, "Cleanup callback must not be null!"); + + return new When<>(stimulus, consumer, customizer); + } + // Customize /** @@ -319,7 +292,7 @@ public class Scenario { * @see #andWaitForStateChange(Supplier) */ public StateChangeResult forStateChange(Supplier supplier) { - return andWaitForStateChange(supplier); + return forStateChange(supplier, DEFAULT_ACCEPTANCE); } /** @@ -347,8 +320,10 @@ public class Scenario { } /** - * Expects a particular state change on the module to produce a result and uses the given {@link Predicate} to - * determine whether the value is conclusive. + * Expects a particular state change on the module to produce a result. By default, a non-{@literal null} value + * would indicate success, except for {@link java.util.Optional}s, in which case we'd check for the presence of a + * value and {@code booleans}, for which we accept {@literal true} as conclusive signal. For more control about the + * result matching, use {@link #andWaitForStateChange(Supplier, Predicate)}. * * @param the type of the result. * @param supplier must not be {@literal null}. @@ -356,16 +331,12 @@ public class Scenario { * @see #forStateChange(Supplier) */ public StateChangeResult andWaitForStateChange(Supplier supplier) { - - Predicate acceptanceCriteria = it -> it instanceof Optional o ? o.isPresent() : it != null; - - return andWaitForStateChange(supplier, acceptanceCriteria); + return andWaitForStateChange(supplier, DEFAULT_ACCEPTANCE); } /** - * Expects a particular state change on the module to produce a result. By default, a non-{@literal null} value - * would indicate success, except for {@link java.util.Optional}s, in which case we'd check for the presence of a - * value. For more control about the result matching, use {@link #andWaitForStateChange(Supplier, Predicate)} + * Expects a particular state change on the module to produce a result and uses the given {@link Predicate} to + * determine whether the value is conclusive. * * @param the type of the result for the state change * @param supplier must not be {@literal null}. diff --git a/spring-modulith-test/src/test/java/org/springframework/modulith/test/ScenarioUnitTests.java b/spring-modulith-test/src/test/java/org/springframework/modulith/test/ScenarioUnitTests.java index 794434a9..1c696699 100644 --- a/spring-modulith-test/src/test/java/org/springframework/modulith/test/ScenarioUnitTests.java +++ b/spring-modulith-test/src/test/java/org/springframework/modulith/test/ScenarioUnitTests.java @@ -83,7 +83,7 @@ class ScenarioUnitTests { @Test // GH-136 void matchesEventsWithPredicate() throws Throwable { - Consumer consumer = it -> publishObject(it) + Consumer consumer = it -> it.publish(() -> 41) .forEventOfType(SomeEvent.class) .matching(__ -> __.payload().startsWith("foo")) .toArrive(); @@ -194,7 +194,8 @@ class ScenarioUnitTests { BiConsumer verification = mock(BiConsumer.class); Consumer cleanupCallback = mock(Consumer.class); - Consumer consumer = it -> it.stimulate(() -> 41, cleanupCallback) + Consumer consumer = it -> it.stimulate(() -> 41) + .andCleanup(cleanupCallback) .forEventOfType(String.class) .toArriveAndVerify(verification); @@ -211,9 +212,10 @@ class ScenarioUnitTests { void triggersCleanupOnFailure() { BiConsumer verification = mock(BiConsumer.class); - Consumer cleanupCallback = mock(Consumer.class); + Runnable cleanupCallback = mock(Runnable.class); - Consumer consumer = it -> it.stimulate((tx) -> 41, cleanupCallback) + Consumer consumer = it -> it.stimulate((tx) -> 41) + .andCleanup(cleanupCallback) .andWaitAtMost(WAIT_TIME) .forEventOfType(String.class) .toArriveAndVerify(verification); @@ -223,7 +225,7 @@ class ScenarioUnitTests { .expectFailure(); verify(verification, never()).accept(any(), any()); - verify(cleanupCallback).accept(41); + verify(cleanupCallback).run(); } @Test // GH-136 @@ -266,8 +268,8 @@ class ScenarioUnitTests { Consumer verification = mock(Consumer.class); - Consumer consumer = it -> it.stimulate(() -> 41) - .andWaitForStateChange(delayed("Foo")) + Consumer consumer = it -> it.stimulate(() -> {}) + .forStateChange(delayed("Foo")) .andVerify(verification); givenAScenario(consumer)