GH-136 - Introduce Scenario as application module integration test API.
See the Javadoc of Scenario for details.
This commit is contained in:
@@ -39,6 +39,11 @@
|
||||
<artifactId>spring-test</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-tx</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
@@ -59,7 +64,6 @@
|
||||
<dependency>
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -47,8 +47,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
@BootstrapWith(SpringBootTestContextBootstrapper.class)
|
||||
@TypeExcludeFilters(ModuleTypeExcludeFilter.class)
|
||||
@ImportAutoConfiguration(ModuleTestAutoConfiguration.class)
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ExtendWith(PublishedEventsParameterResolver.class)
|
||||
@ExtendWith({ SpringExtension.class, PublishedEventsParameterResolver.class, ScenarioParameterResolver.class })
|
||||
@TestInstance(Lifecycle.PER_CLASS)
|
||||
@TestConstructor(autowireMode = AutowireMode.ALL)
|
||||
public @interface ApplicationModuleTest {
|
||||
|
||||
@@ -100,13 +100,13 @@ class DefaultPublishedEvents implements PublishedEvents, ApplicationListener<App
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.test.PublishedEvents.TypedPublishedEvents#ofSubType(java.lang.Class)
|
||||
* @see org.springframework.modulith.test.PublishedEvents.TypedPublishedEvents#ofType(java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <S extends T> TypedPublishedEvents<S> ofSubType(Class<S> subType) {
|
||||
public <S> TypedPublishedEvents<S> ofType(Class<S> type) {
|
||||
|
||||
return SimpleTypedPublishedEvents.of(getFilteredEvents(subType::isInstance) //
|
||||
.map(subType::cast));
|
||||
return SimpleTypedPublishedEvents.of(getFilteredEvents(type::isInstance) //
|
||||
.map(type::cast));
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public interface PublishedEvents {
|
||||
public interface PublishedEvents extends TypedEvents {
|
||||
|
||||
/**
|
||||
* Creates a new {@link PublishedEvents} instance for the given events.
|
||||
@@ -53,35 +53,19 @@ public interface PublishedEvents {
|
||||
return new DefaultPublishedEvents(events);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all application events of the given type that were fired during the test execution.
|
||||
*
|
||||
* @param <T> the event type
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.test.TypedEvents#ofType(java.lang.Class)
|
||||
*/
|
||||
<T> TypedPublishedEvents<T> ofType(Class<T> type);
|
||||
|
||||
/**
|
||||
* Returns whether an event of the given type was published.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return whether an event of the given type was published.
|
||||
*/
|
||||
default boolean eventOfTypeWasPublished(Class<?> type) {
|
||||
|
||||
Assert.notNull(type, "Event type must not be null!");
|
||||
|
||||
return ofType(type).iterator().hasNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* All application events of a given type that were fired during a test execution.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @param <T> the event type
|
||||
*/
|
||||
interface TypedPublishedEvents<T> extends Iterable<T> {
|
||||
interface TypedPublishedEvents<T> extends Iterable<T>, TypedEvents {
|
||||
|
||||
/**
|
||||
* Further constrain the event type for downstream assertions.
|
||||
@@ -90,7 +74,16 @@ public interface PublishedEvents {
|
||||
* @param subType the sub type
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
<S extends T> TypedPublishedEvents<S> ofSubType(Class<S> subType);
|
||||
default <S extends T> TypedPublishedEvents<S> ofSubType(Class<S> subType) {
|
||||
return ofType(subType);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.test.TypedEvents#ofType(java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
<S> TypedPublishedEvents<S> ofType(Class<S> type);
|
||||
|
||||
/**
|
||||
* Returns all {@link TypedPublishedEvents} that match the given predicate.
|
||||
|
||||
@@ -31,14 +31,14 @@ import org.springframework.util.Assert;
|
||||
* @author Oliver Drotbohm
|
||||
* @see AssertablePublishedEvents
|
||||
*/
|
||||
public class PublishedEventsAssert extends AbstractAssert<PublishedEventsAssert, PublishedEvents> {
|
||||
public class PublishedEventsAssert extends AbstractAssert<PublishedEventsAssert, TypedEvents> {
|
||||
|
||||
/**
|
||||
* Creates a new {@link PublishedEventsAssert}
|
||||
*
|
||||
* @param actual must not be {@literal null}.
|
||||
*/
|
||||
PublishedEventsAssert(AssertablePublishedEvents actual) {
|
||||
PublishedEventsAssert(TypedEvents actual) {
|
||||
super(actual, PublishedEventsAssert.class);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,580 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
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;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.apiguardian.api.API.Status;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.awaitility.core.ConditionFactory;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.lang.Nullable;
|
||||
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.support.TransactionOperations;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.tngtech.archunit.thirdparty.com.google.common.base.Optional;
|
||||
|
||||
/**
|
||||
* A DSL to define integration testing scenarios for application modules. A {@link Scenario} starts with a stimulus on
|
||||
* the system, usually a component invocation (see {@link #stimulate(Function)} or event publication (see
|
||||
* {@link #publish(Object...)}) and a definition of the expected outcome. That can be a state change observed by
|
||||
* invoking application module components (see {@link When#andWaitForStateChange(Supplier)}) or another event being
|
||||
* published (see {@link When#andWaitForEventOfType(Class)}), concluded by additional verifications.
|
||||
* <p>
|
||||
* {@link Scenario} can be used as JUnit test method parameter in {@link ApplicationModuleTest}s.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @see ApplicationModuleTest
|
||||
*/
|
||||
@API(status = Status.EXPERIMENTAL)
|
||||
public class Scenario {
|
||||
|
||||
private final TransactionOperations transactionOperations;
|
||||
private final ApplicationEventPublisher publisher;
|
||||
private final AssertablePublishedEvents events;
|
||||
|
||||
/**
|
||||
* Creates a new {@link Scenario} for the given {@link TransactionOperations}, {@link ApplicationEventPublisher} and
|
||||
* {@link AssertablePublishedEvents}.
|
||||
*
|
||||
* @param transactionOperations must not be {@literal null}.
|
||||
* @param publisher must not be {@literal null}.
|
||||
* @param events must not be {@literal null}.
|
||||
*/
|
||||
Scenario(TransactionOperations transactionOperations, ApplicationEventPublisher publisher,
|
||||
AssertablePublishedEvents events) {
|
||||
|
||||
Assert.notNull(transactionOperations, "TransactionOperations must not be null!");
|
||||
Assert.notNull(publisher, "ApplicationEventPublisher must not be null!");
|
||||
Assert.notNull(events, "AssertablePublishedEvents must not be null!");
|
||||
|
||||
this.transactionOperations = transactionOperations;
|
||||
this.publisher = publisher;
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes the given event. The event will be published in a transaction to make sure that transactional event
|
||||
* listeners are invoked as well.
|
||||
*
|
||||
* @param event must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public When<Void> publish(Object... event) {
|
||||
return stimulate((tx, e) -> {
|
||||
tx.executeWithoutResult(__ -> Arrays.stream(event).forEach(e::publishEvent));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes the event provided by the given {@link Supplier}. The event will be published in a transaction to make
|
||||
* sure that transactional event listeners are invoked as well.
|
||||
*
|
||||
* @param events must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public When<Void> publish(Supplier<Object>... events) {
|
||||
|
||||
return stimulate((tx, e) -> {
|
||||
tx.executeWithoutResult(__ -> Arrays.stream(events).map(Supplier::get).forEach(e::publishEvent));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stimulates the system by executing the given {@link Runnable}.
|
||||
*
|
||||
* @param runnable must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public When<Void> stimulate(Runnable runnable) {
|
||||
|
||||
Assert.notNull(runnable, "Runnable must not be null!");
|
||||
|
||||
return stimulate(() -> {
|
||||
runnable.run();
|
||||
return null;
|
||||
}, __ -> {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stimulates the system using the given {@link Supplier} and keeping the supplied value around for later
|
||||
* verification.
|
||||
*
|
||||
* @param <S> the type of the value returned by the stimulus.
|
||||
* @param supplier must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @see StateChangeResult#andVerify(Consumer)
|
||||
* @see EventResult#toArriveAndVerify(Consumer)
|
||||
*/
|
||||
public <S> When<S> stimulate(Supplier<S> 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 <S> 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 <S> When<S> stimulate(Supplier<S> supplier, Consumer<S> cleanupCallback) {
|
||||
|
||||
Assert.notNull(supplier, "Supplier must not be null!");
|
||||
Assert.notNull(cleanupCallback, "Cleanup callback must not be null!");
|
||||
|
||||
return stimulate((tx, e) -> supplier.get(), cleanupCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stimulates the system using the given function providing access to the {@link TransactionOperations} and keeping
|
||||
* the supplied value around for later verification.
|
||||
*
|
||||
* @param <S> the type of the value returned by the stimulus.
|
||||
* @param function must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public <S> When<S> stimulate(Function<TransactionOperations, S> 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 <S> 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 <S> When<S> stimulate(Function<TransactionOperations, S> function, Consumer<S> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stimulate the system using the given {@link TransactionOperations} and {@link ApplicationEventPublisher}. Usually a
|
||||
* method on some application service or event publication is triggered.
|
||||
*
|
||||
* @param stimulus must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public When<Void> stimulate(BiConsumer<TransactionOperations, ApplicationEventPublisher> stimulus) {
|
||||
|
||||
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<Void> stimulate(BiConsumer<TransactionOperations, ApplicationEventPublisher> 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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Stimulate the system using the given {@link TransactionOperations} and {@link ApplicationEventPublisher} and
|
||||
* produce a result. Usually a method on some application service or event publication is triggered.
|
||||
*
|
||||
* @param <S> the type of the result.
|
||||
* @param stimulus must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public <S> When<S> stimulate(BiFunction<TransactionOperations, ApplicationEventPublisher, S> stimulus) {
|
||||
|
||||
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 <S> 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 <S> When<S> stimulate(BiFunction<TransactionOperations, ApplicationEventPublisher, S> stimulus,
|
||||
Consumer<S> 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());
|
||||
}
|
||||
|
||||
public class When<T> {
|
||||
|
||||
private final BiFunction<TransactionOperations, ApplicationEventPublisher, T> stimulus;
|
||||
private final Consumer<T> cleanup;
|
||||
private final Function<ConditionFactory, ConditionFactory> customizer;
|
||||
|
||||
/**
|
||||
* @param stimulus must not be {@literal null}.
|
||||
* @param cleanup must not be {@literal null}.
|
||||
* @param customizer must not be {@literal null}.
|
||||
*/
|
||||
When(BiFunction<TransactionOperations, ApplicationEventPublisher, T> stimulus, Consumer<T> cleanup,
|
||||
Function<ConditionFactory, ConditionFactory> customizer) {
|
||||
this.stimulus = stimulus;
|
||||
this.cleanup = cleanup;
|
||||
this.customizer = customizer;
|
||||
}
|
||||
|
||||
// Customize
|
||||
|
||||
/**
|
||||
* Configures the {@link Scenario} to wait for at most the given duration for an event of the subsequent
|
||||
* specification to arrive.
|
||||
*
|
||||
* @param duration must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public When<T> andWaitAtMost(Duration duration) {
|
||||
|
||||
Assert.notNull(duration, "Duration must not be null!");
|
||||
|
||||
return customize(it -> it.atMost(duration));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param customizer must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public When<T> customize(Function<ConditionFactory, ConditionFactory> customizer) {
|
||||
|
||||
Assert.notNull(customizer, "Customizer must not be null!");
|
||||
|
||||
return new When<T>(stimulus, cleanup, customizer);
|
||||
}
|
||||
|
||||
// Expect event
|
||||
|
||||
/**
|
||||
* Alternative to {@link #andWaitForEventOfType(Class)} for better readability if execution customizations have been
|
||||
* applied before.
|
||||
*
|
||||
* @param <E> the type of the event.
|
||||
* @param type must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @see #andWaitForEventOfType(Class)
|
||||
*/
|
||||
public <E> EventResult<E> forEventOfType(Class<E> type) {
|
||||
return andWaitForEventOfType(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative to {@link #andWaitForStateChange(Supplier)} for better readability if execution customizations have
|
||||
* been applied before.
|
||||
*
|
||||
* @param <S> the type of the state change result
|
||||
* @param supplier must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @see #andWaitForStateChange(Supplier)
|
||||
*/
|
||||
public <S> StateChangeResult<S> forStateChange(Supplier<S> supplier) {
|
||||
return andWaitForStateChange(supplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative to {@link #andWaitForStateChange(Supplier, Predicate)} for better readability if execution
|
||||
* customizations have been applied before.
|
||||
*
|
||||
* @param <S> the type of the state change result
|
||||
* @param supplier must not be {@literal null}.
|
||||
* @param acceptanceCriteria must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @see #andWaitForStateChange(Supplier, Predicate)
|
||||
*/
|
||||
public <S> StateChangeResult<S> forStateChange(Supplier<S> supplier, Predicate<? super S> acceptanceCriteria) {
|
||||
return andWaitForStateChange(supplier, acceptanceCriteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param <E> the type of the event.
|
||||
* @param type must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @see #forEventOfType(Class)
|
||||
*/
|
||||
public <E> EventResult<E> andWaitForEventOfType(Class<E> type) {
|
||||
return new EventResult<E>(type, Function.identity());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 <S> the type of the result.
|
||||
* @param supplier must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @see #forStateChange(Supplier)
|
||||
*/
|
||||
public <S> StateChangeResult<S> andWaitForStateChange(Supplier<S> supplier) {
|
||||
|
||||
Predicate<S> acceptanceCriteria = it -> it instanceof Optional<?> o ? o.isPresent() : it != null;
|
||||
|
||||
return andWaitForStateChange(supplier, acceptanceCriteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)}
|
||||
*
|
||||
* @param <S> the type of the result for the state change
|
||||
* @param supplier must not be {@literal null}.
|
||||
* @param acceptanceCriteria must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @see #andWaitForStateChange(Supplier, Predicate)
|
||||
*/
|
||||
public <S> StateChangeResult<S> andWaitForStateChange(Supplier<S> supplier,
|
||||
Predicate<? super S> acceptanceCriteria) {
|
||||
|
||||
Assert.notNull(supplier, "Supplier must not be null!");
|
||||
Assert.notNull(acceptanceCriteria, "Acceptance criteria must not be null!");
|
||||
|
||||
return new StateChangeResult<>(awaitInternal(__ -> {}, () -> supplier.get(), acceptanceCriteria));
|
||||
}
|
||||
|
||||
private <S> ExecutionResult<S, T> awaitInternal(Consumer<T> verifications, Callable<S> supplier,
|
||||
Predicate<? super S> condition) {
|
||||
|
||||
T result = stimulus.apply(transactionOperations, publisher);
|
||||
|
||||
try {
|
||||
|
||||
S foo = customizer.apply(Awaitility.await())
|
||||
.until(supplier, condition);
|
||||
|
||||
verifications.accept(result);
|
||||
|
||||
return new ExecutionResult<>(foo, result);
|
||||
|
||||
} finally {
|
||||
cleanup.accept(result);
|
||||
}
|
||||
}
|
||||
|
||||
private record ExecutionResult<S, T>(S first, T second) {}
|
||||
|
||||
public class StateChangeResult<S> {
|
||||
|
||||
private ExecutionResult<S, T> result;
|
||||
|
||||
StateChangeResult(ExecutionResult<S, T> result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the state change result using the given {@link Consumer}.
|
||||
*
|
||||
* @param consumer must not be {@literal null}.
|
||||
*/
|
||||
public void andVerify(Consumer<S> consumer) {
|
||||
|
||||
Assert.notNull(consumer, "Consumer must not be null!");
|
||||
|
||||
consumer.accept(result.first());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the state change result and stimulus result using the given {@link BiConsumer}.
|
||||
*
|
||||
* @param consumer must not be {@literal null}.
|
||||
*/
|
||||
public void andVerify(BiConsumer<S, T> consumer) {
|
||||
|
||||
Assert.notNull(consumer, "BiConsumer must not be null!");
|
||||
|
||||
consumer.accept(result.first(), result.second());
|
||||
}
|
||||
}
|
||||
|
||||
public class EventResult<E> {
|
||||
|
||||
private final Class<E> type;
|
||||
private final Function<TypedPublishedEvents<E>, TypedPublishedEvents<E>> filter;
|
||||
|
||||
/**
|
||||
* Creates a new {@link EventResult} for the given type and filter.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @param filtered must not be {@literal null}.
|
||||
*/
|
||||
EventResult(Class<E> type, Function<TypedPublishedEvents<E>, TypedPublishedEvents<E>> filtered) {
|
||||
|
||||
Assert.notNull(type, "Event type must not be null!");
|
||||
|
||||
this.type = type;
|
||||
this.filter = filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches events that satisfy the given {@link Predicate}.
|
||||
*
|
||||
* @param filter must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public EventResult<E> matching(Predicate<? super E> filter) {
|
||||
|
||||
Assert.notNull(filter, "Filter must not be null!");
|
||||
|
||||
return new EventResult<E>(type, createOrAdd(it -> it.matching(filter)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches events that satisfy the given {@link Predicate} after extracting a value using the given
|
||||
* {@link Function}.
|
||||
*
|
||||
* @param <S> the type of the extracted value.
|
||||
* @param extractor must not be {@literal null}.
|
||||
* @param filter must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public <S> EventResult<E> matchingMapped(Function<E, S> extractor, Predicate<? super S> filter) {
|
||||
return new EventResult<E>(type, createOrAdd(it -> it.matching(extractor, filter)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches events that extracting the given value using the given {@link Function}.
|
||||
*
|
||||
* @param <S> the type of the extracted value.
|
||||
* @param extractor must not be {@literal null}.
|
||||
* @param value can be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public <S> EventResult<E> matchingMappedValue(Function<E, S> extractor, @Nullable S value) {
|
||||
return new EventResult<E>(type, createOrAdd(it -> it.matching(extractor, value)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Awaits an event of the given specification to arrive.
|
||||
*/
|
||||
public void toArrive() {
|
||||
toArriveAndVerifyInternal(__ -> {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Awaits an event of the given specification to arrive and invokes the given consumer with it.
|
||||
*
|
||||
* @param consumer must not be {@literal null}.
|
||||
*/
|
||||
public void toArriveAndVerify(Consumer<E> consumer) {
|
||||
|
||||
Assert.notNull(consumer, "Consumer must not be null!");
|
||||
|
||||
toArriveAndVerifyInternal(__ -> {
|
||||
consumer.accept(getFilteredEvents().iterator().next());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Awaits an event of the given specification to arrive and invokes the given consumer with it as well as the
|
||||
* result created by the stimulus.
|
||||
*
|
||||
* @param consumer must not be {@literal null}.
|
||||
*/
|
||||
public void toArriveAndVerify(BiConsumer<E, T> consumer) {
|
||||
|
||||
Assert.notNull(consumer, "Consumer must not be null!");
|
||||
|
||||
toArriveAndVerifyInternal(it -> {
|
||||
consumer.accept(getFilteredEvents().iterator().next(), it);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects the events previously specified to arrive and additionally assert the {@link PublishedEventsAssert} for
|
||||
* the captured event.
|
||||
*
|
||||
* @param consumer must not be {@literal null}.
|
||||
*/
|
||||
public void toArriveAndAssert(Consumer<PublishedEventAssert<? super E>> consumer) {
|
||||
|
||||
Assert.notNull(consumer, "Consumer must not be null!");
|
||||
|
||||
toArriveAndVerifyInternal(__ -> {
|
||||
consumer.accept(getAssertedEvent());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects the events previously specified to arrive and additionally assert the {@link PublishedEventsAssert} for
|
||||
* the captured event and original stimulus result.
|
||||
*
|
||||
* @param consumer must not be {@literal null}.
|
||||
*/
|
||||
public void toArriveAndAssert(BiConsumer<PublishedEventAssert<? super E>, T> consumer) {
|
||||
|
||||
Assert.notNull(consumer, "Consumer must not be null!");
|
||||
|
||||
toArriveAndVerifyInternal(it -> {
|
||||
consumer.accept(getAssertedEvent(), it);
|
||||
});
|
||||
}
|
||||
|
||||
private Function<TypedPublishedEvents<E>, TypedPublishedEvents<E>> createOrAdd(
|
||||
Function<TypedPublishedEvents<E>, TypedPublishedEvents<E>> filter) {
|
||||
return this.filter == null ? filter : this.filter.andThen(filter);
|
||||
}
|
||||
|
||||
private TypedPublishedEvents<E> getFilteredEvents() {
|
||||
return filter.apply(events.ofType(type));
|
||||
}
|
||||
|
||||
private PublishedEventAssert<? super E> getAssertedEvent() {
|
||||
return new PublishedEventsAssert(getFilteredEvents()).contains(type);
|
||||
}
|
||||
|
||||
private void toArriveAndVerifyInternal(Consumer<T> verifications) {
|
||||
awaitInternal(verifications, () -> getFilteredEvents(), it -> it.eventOfTypeWasPublished(type));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.modulith.test;
|
||||
|
||||
import org.junit.jupiter.api.extension.BeforeAllCallback;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
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;
|
||||
|
||||
/**
|
||||
* JUnit {@link ParameterResolver} for {@link Scenario}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class ScenarioParameterResolver implements ParameterResolver, BeforeAllCallback {
|
||||
|
||||
private final PublishedEventsParameterResolver delegate;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ScenarioParameterResolver}.
|
||||
*/
|
||||
public ScenarioParameterResolver() {
|
||||
this.delegate = new PublishedEventsParameterResolver();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.junit.jupiter.api.extension.ParameterResolver#supportsParameter(org.junit.jupiter.api.extension.ParameterContext, org.junit.jupiter.api.extension.ExtensionContext)
|
||||
*/
|
||||
@Override
|
||||
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
|
||||
throws ParameterResolutionException {
|
||||
|
||||
var type = parameterContext.getParameter().getType();
|
||||
|
||||
return Scenario.class.isAssignableFrom(type);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.junit.jupiter.api.extension.BeforeAllCallback#beforeAll(org.junit.jupiter.api.extension.ExtensionContext)
|
||||
*/
|
||||
@Override
|
||||
public void beforeAll(ExtensionContext context) throws Exception {
|
||||
delegate.beforeAll(context);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.junit.jupiter.api.extension.ParameterResolver#resolveParameter(org.junit.jupiter.api.extension.ParameterContext, org.junit.jupiter.api.extension.ExtensionContext)
|
||||
*/
|
||||
@Override
|
||||
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
|
||||
throws ParameterResolutionException {
|
||||
|
||||
var context = SpringExtension.getApplicationContext(extensionContext);
|
||||
var operations = context.getBean(TransactionOperations.class);
|
||||
var events = (AssertablePublishedEvents) delegate.resolveParameter(parameterContext, extensionContext);
|
||||
|
||||
return new Scenario(operations, context, events);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.modulith.test;
|
||||
|
||||
import org.springframework.modulith.test.PublishedEvents.TypedPublishedEvents;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
interface TypedEvents {
|
||||
|
||||
/**
|
||||
* Returns all events of the given type.
|
||||
*
|
||||
* @param <T> the event type
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
<T> TypedPublishedEvents<T> ofType(Class<T> type);
|
||||
|
||||
/**
|
||||
* Returns whether an event of the given type was published.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return whether an event of the given type was published.
|
||||
*/
|
||||
default boolean eventOfTypeWasPublished(Class<?> type) {
|
||||
|
||||
Assert.notNull(type, "Event type must not be null!");
|
||||
|
||||
return ofType(type).iterator().hasNext();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.modulith.test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.lang.Thread.UncaughtExceptionHandler;
|
||||
import java.time.Duration;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
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.support.TransactionOperations;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link Scenario}.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ScenarioUnitTests {
|
||||
|
||||
private static final Duration DELAY = Duration.ofMillis(50);
|
||||
private static final Duration WAIT_TIME = Duration.ofMillis(101);
|
||||
private static final Duration TIMED_OUT = Duration.ofMillis(150);
|
||||
|
||||
@Mock TransactionOperations tx;
|
||||
@Mock ApplicationEventPublisher publisher;
|
||||
|
||||
@Test // GH-136
|
||||
void timesOutIfNoEventArrivesInTime() throws Throwable {
|
||||
|
||||
Consumer<Scenario> consumer = it -> publishObject(it)
|
||||
.forEventOfType(String.class)
|
||||
.toArrive();
|
||||
|
||||
givenAScenario(consumer)
|
||||
.waitFor(TIMED_OUT)
|
||||
.onEvent(() -> "foo")
|
||||
.expectFailure();
|
||||
}
|
||||
|
||||
@Test // GH-136
|
||||
void failsIfNoEventOfExpectedTypeArrives() {
|
||||
|
||||
Consumer<Scenario> consumer = it -> publishObject(it)
|
||||
.forEventOfType(String.class)
|
||||
.toArrive();
|
||||
|
||||
givenAScenario(consumer)
|
||||
.onEvent(() -> 1L)
|
||||
.expectFailure();
|
||||
}
|
||||
|
||||
@Test // GH-136
|
||||
void matchesEventsWithPredicate() throws Throwable {
|
||||
|
||||
Consumer<Scenario> consumer = it -> publishObject(it)
|
||||
.forEventOfType(SomeEvent.class)
|
||||
.matching(__ -> __.payload().startsWith("foo"))
|
||||
.toArrive();
|
||||
|
||||
givenAScenario(consumer)
|
||||
.onEvent(() -> new SomeEvent("foo"))
|
||||
.expectSuccess();
|
||||
}
|
||||
|
||||
@Test // GH-136
|
||||
void matchesEventsExtractingPayloadWithPredicate() throws Throwable {
|
||||
|
||||
Consumer<Scenario> consumer = it -> publishObject(it)
|
||||
.forEventOfType(SomeEvent.class)
|
||||
.matchingMapped(SomeEvent::payload, __ -> __.startsWith("foo"))
|
||||
.toArrive();
|
||||
|
||||
givenAScenario(consumer)
|
||||
.onEvent(() -> new SomeEvent("foo"))
|
||||
.expectSuccess();
|
||||
}
|
||||
|
||||
@Test // GH-136
|
||||
void matchesEventsExtractingPayloadWithValue() throws Throwable {
|
||||
|
||||
Consumer<Scenario> consumer = it -> publishObject(it)
|
||||
.forEventOfType(SomeEvent.class)
|
||||
.matchingMappedValue(SomeEvent::payload, "foo")
|
||||
.toArrive();
|
||||
|
||||
givenAScenario(consumer)
|
||||
.onEvent(() -> new SomeEvent("foo"))
|
||||
.expectSuccess();
|
||||
}
|
||||
|
||||
@Test // GH-136
|
||||
@SuppressWarnings("unchecked")
|
||||
void triggersVerificationOnSuccess() {
|
||||
|
||||
Consumer<String> verification = mock(Consumer.class);
|
||||
|
||||
Consumer<Scenario> consumer = it -> publishObject(it)
|
||||
.forEventOfType(String.class)
|
||||
.toArriveAndVerify(verification);
|
||||
|
||||
givenAScenario(consumer)
|
||||
.onEvent(() -> "foo")
|
||||
.expectSuccess();
|
||||
|
||||
verify(verification).accept("foo");
|
||||
}
|
||||
|
||||
@Test // GH-136
|
||||
@SuppressWarnings("unchecked")
|
||||
void triggersVerificationWithResultOnSuccess() {
|
||||
|
||||
BiConsumer<String, Integer> verification = mock(BiConsumer.class);
|
||||
|
||||
Consumer<Scenario> consumer = it -> it.stimulate(() -> 41)
|
||||
.forEventOfType(String.class)
|
||||
.toArriveAndVerify(verification);
|
||||
|
||||
givenAScenario(consumer)
|
||||
.onEvent(() -> "foo")
|
||||
.expectSuccess();
|
||||
|
||||
verify(verification).accept("foo", 41);
|
||||
}
|
||||
|
||||
@Test // GH-136
|
||||
@SuppressWarnings("unchecked")
|
||||
void doesNotTriggerVerificationOnFailure() {
|
||||
|
||||
Consumer<String> verification = mock(Consumer.class);
|
||||
|
||||
Consumer<Scenario> consumer = it -> publishObject(it)
|
||||
.forEventOfType(String.class)
|
||||
.toArriveAndVerify(verification);
|
||||
|
||||
givenAScenario(consumer)
|
||||
.onEvent(() -> 1)
|
||||
.expectFailure();
|
||||
|
||||
verify(verification, never()).accept(any());
|
||||
}
|
||||
|
||||
@Test // GH-136
|
||||
@SuppressWarnings("unchecked")
|
||||
void doesNotTriggerVerificationWithResultOnFailure() {
|
||||
|
||||
BiConsumer<String, Void> verification = mock(BiConsumer.class);
|
||||
|
||||
Consumer<Scenario> consumer = it -> publishObject(it)
|
||||
.forEventOfType(String.class)
|
||||
.toArriveAndVerify(verification);
|
||||
|
||||
givenAScenario(consumer)
|
||||
.onEvent(() -> 1)
|
||||
.expectFailure();
|
||||
|
||||
verify(verification, never()).accept(any(), any());
|
||||
}
|
||||
|
||||
@Test // GH-136
|
||||
@SuppressWarnings("unchecked")
|
||||
void triggersCleanupOnSuccess() {
|
||||
|
||||
BiConsumer<String, Integer> verification = mock(BiConsumer.class);
|
||||
Consumer<Integer> cleanupCallback = mock(Consumer.class);
|
||||
|
||||
Consumer<Scenario> consumer = it -> it.stimulate(() -> 41, cleanupCallback)
|
||||
.forEventOfType(String.class)
|
||||
.toArriveAndVerify(verification);
|
||||
|
||||
givenAScenario(consumer)
|
||||
.onEvent(() -> "foo")
|
||||
.expectSuccess();
|
||||
|
||||
verify(verification).accept("foo", 41);
|
||||
verify(cleanupCallback).accept(41);
|
||||
}
|
||||
|
||||
@Test // GH-136
|
||||
@SuppressWarnings("unchecked")
|
||||
void triggersCleanupOnFailure() {
|
||||
|
||||
BiConsumer<String, Integer> verification = mock(BiConsumer.class);
|
||||
Consumer<Integer> cleanupCallback = mock(Consumer.class);
|
||||
|
||||
Consumer<Scenario> consumer = it -> it.stimulate((tx) -> 41, cleanupCallback)
|
||||
.andWaitAtMost(WAIT_TIME)
|
||||
.forEventOfType(String.class)
|
||||
.toArriveAndVerify(verification);
|
||||
|
||||
givenAScenario(consumer)
|
||||
.onEvent(() -> 4711)
|
||||
.expectFailure();
|
||||
|
||||
verify(verification, never()).accept(any(), any());
|
||||
verify(cleanupCallback).accept(41);
|
||||
}
|
||||
|
||||
@Test // GH-136
|
||||
@SuppressWarnings("unchecked")
|
||||
void triggersAssertOnSuccess() {
|
||||
|
||||
Consumer<PublishedEventAssert<? super String>> verification = mock(Consumer.class);
|
||||
|
||||
Consumer<Scenario> consumer = it -> publishObject(it)
|
||||
.forEventOfType(String.class)
|
||||
.toArriveAndAssert(verification);
|
||||
|
||||
givenAScenario(consumer)
|
||||
.onEvent(() -> "foo")
|
||||
.expectSuccess();
|
||||
|
||||
verify(verification).accept(any());
|
||||
}
|
||||
|
||||
@Test // GH-136
|
||||
@SuppressWarnings("unchecked")
|
||||
void triggersAssertWithResultOnSuccess() {
|
||||
|
||||
BiConsumer<PublishedEventAssert<? super String>, Integer> verification = mock(BiConsumer.class);
|
||||
|
||||
Consumer<Scenario> consumer = it -> it.stimulate((tx) -> 41)
|
||||
.forEventOfType(String.class)
|
||||
.toArriveAndAssert(verification);
|
||||
|
||||
givenAScenario(consumer)
|
||||
.onEvent(() -> "foo")
|
||||
.expectSuccess();
|
||||
|
||||
verify(verification).accept(any(), eq(41));
|
||||
}
|
||||
|
||||
@Test // GH-136
|
||||
@SuppressWarnings("unchecked")
|
||||
void triggersVerificationOnSuccessfulStateChange() {
|
||||
|
||||
Consumer<String> verification = mock(Consumer.class);
|
||||
|
||||
Consumer<Scenario> consumer = it -> it.stimulate(() -> 41)
|
||||
.andWaitForStateChange(delayed("Foo"))
|
||||
.andVerify(verification);
|
||||
|
||||
givenAScenario(consumer)
|
||||
.expectSuccess();
|
||||
|
||||
verify(verification).accept("Foo");
|
||||
}
|
||||
|
||||
@Test // GH-136
|
||||
@SuppressWarnings("unchecked")
|
||||
void triggersVerificationOnSuccessfulStateChangeWithResult() {
|
||||
|
||||
BiConsumer<String, Integer> verification = mock(BiConsumer.class);
|
||||
|
||||
Consumer<Scenario> consumer = it -> it.stimulate(() -> 41)
|
||||
.andWaitForStateChange(delayed("Foo"))
|
||||
.andVerify(verification);
|
||||
|
||||
givenAScenario(consumer)
|
||||
.expectSuccess();
|
||||
|
||||
verify(verification).accept("Foo", 41);
|
||||
}
|
||||
|
||||
@Test // GH-136
|
||||
@SuppressWarnings("unchecked")
|
||||
void doesNotTriggerVerificationOnFailedStateChange() {
|
||||
|
||||
Consumer<String> verification = mock(Consumer.class);
|
||||
|
||||
Consumer<Scenario> consumer = it -> it.stimulate(() -> 41)
|
||||
.andWaitAtMost(WAIT_TIME)
|
||||
.forStateChange(delayed("Foo", TIMED_OUT))
|
||||
.andVerify(verification);
|
||||
|
||||
givenAScenario(consumer)
|
||||
.expectFailure();
|
||||
|
||||
verify(verification, never()).accept(any());
|
||||
}
|
||||
|
||||
@Test // GH-136
|
||||
@SuppressWarnings("unchecked")
|
||||
void doesNotTriggerVerificationWithResultOnFailedStateChange() {
|
||||
|
||||
BiConsumer<String, Integer> verification = mock(BiConsumer.class);
|
||||
|
||||
Consumer<Scenario> consumer = it -> it.stimulate(() -> 41)
|
||||
.andWaitAtMost(WAIT_TIME)
|
||||
.forStateChange(delayed("Foo", TIMED_OUT))
|
||||
.andVerify(verification);
|
||||
|
||||
givenAScenario(consumer)
|
||||
.expectFailure();
|
||||
|
||||
verify(verification, never()).accept(any(), any());
|
||||
}
|
||||
|
||||
private Fixture givenAScenario(Consumer<Scenario> consumer) {
|
||||
return new Fixture(consumer, DELAY, null, new DefaultAssertablePublishedEvents());
|
||||
}
|
||||
|
||||
private static When<Void> publishObject(Scenario source) {
|
||||
return source.publish(new Object())
|
||||
.andWaitAtMost(WAIT_TIME);
|
||||
}
|
||||
|
||||
private static <S> Supplier<S> delayed(S value) {
|
||||
return delayed(value, DELAY);
|
||||
}
|
||||
|
||||
private static <S> Supplier<S> delayed(S value, Duration duration) {
|
||||
|
||||
return () -> {
|
||||
try {
|
||||
Thread.sleep(duration.toMillis());
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
class Fixture {
|
||||
|
||||
private final Consumer<Scenario> consumer;
|
||||
private final Duration duration;
|
||||
private final Runnable runnable;
|
||||
private final DefaultAssertablePublishedEvents events;
|
||||
|
||||
private CapturingExceptionHandler exceptionHandler = new CapturingExceptionHandler();
|
||||
|
||||
public Fixture waitFor(Duration duration) {
|
||||
return new Fixture(consumer, duration, runnable, events);
|
||||
}
|
||||
|
||||
public Fixture onEvent(Supplier<Object> event) {
|
||||
|
||||
Runnable runnable = () -> events.onApplicationEvent(new PayloadApplicationEvent<Object>(this, event.get()));
|
||||
|
||||
return new Fixture(consumer, duration, runnable, events);
|
||||
}
|
||||
|
||||
public void expectSuccess() {
|
||||
|
||||
foo();
|
||||
|
||||
exceptionHandler.throwIfCaught();
|
||||
}
|
||||
|
||||
public void expectFailure() {
|
||||
|
||||
foo();
|
||||
|
||||
assertThat(exceptionHandler.caught)
|
||||
.as("Expected failure but did not see an exception!")
|
||||
.isNotNull();
|
||||
}
|
||||
|
||||
private void foo() {
|
||||
|
||||
try {
|
||||
|
||||
Runnable foo = () -> consumer.accept(new Scenario(tx, publisher, events));
|
||||
|
||||
var thread = new Thread(foo);
|
||||
thread.setUncaughtExceptionHandler(exceptionHandler);
|
||||
thread.start();
|
||||
|
||||
Thread.sleep(duration.toMillis());
|
||||
|
||||
if (runnable != null) {
|
||||
runnable.run();
|
||||
}
|
||||
|
||||
thread.join();
|
||||
|
||||
} catch (Throwable o_O) {
|
||||
|
||||
if (o_O instanceof RuntimeException e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
throw new RuntimeException(o_O);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record SomeEvent(String payload) {}
|
||||
|
||||
static class CapturingExceptionHandler implements UncaughtExceptionHandler {
|
||||
|
||||
Throwable caught;
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Thread.UncaughtExceptionHandler#uncaughtException(java.lang.Thread, java.lang.Throwable)
|
||||
*/
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
caught = e;
|
||||
}
|
||||
|
||||
public void throwIfCaught() {
|
||||
|
||||
if (caught == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (caught instanceof RuntimeException e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
throw new RuntimeException(caught);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user