Skip to content

Commit bb800ba

Browse files
committed
GH-185 - Additional simplification of delayed event verification on Scenarios.
1 parent 3a92351 commit bb800ba

File tree

4 files changed

+110
-8
lines changed

4 files changed

+110
-8
lines changed

spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultAssertablePublishedEvents.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,13 @@ public <T> TypedPublishedEvents<T> ofType(Class<T> type) {
6464
public void onApplicationEvent(ApplicationEvent event) {
6565
delegate.onApplicationEvent(event);
6666
}
67+
68+
/*
69+
* (non-Javadoc)
70+
* @see java.lang.Object#toString()
71+
*/
72+
@Override
73+
public String toString() {
74+
return delegate.toString();
75+
}
6776
}

spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultPublishedEvents.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Objects;
2424
import java.util.function.Function;
2525
import java.util.function.Predicate;
26+
import java.util.stream.Collectors;
2627
import java.util.stream.Stream;
2728

2829
import org.springframework.context.ApplicationEvent;
@@ -79,6 +80,18 @@ public <T> TypedPublishedEvents<T> ofType(Class<T> type) {
7980
.map(type::cast));
8081
}
8182

83+
/*
84+
* (non-Javadoc)
85+
* @see java.lang.Object#toString()
86+
*/
87+
@Override
88+
public String toString() {
89+
90+
return events.isEmpty()
91+
? "[]"
92+
: events.stream().map(Object::toString).collect(Collectors.joining("[ ", ", ", " ]"));
93+
}
94+
8295
private static Object unwrapPayloadEvent(Object source) {
8396

8497
return PayloadApplicationEvent.class.isInstance(source) //

spring-modulith-test/src/main/java/org/springframework/modulith/test/Scenario.java

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
*/
1616
package org.springframework.modulith.test;
1717

18+
import static org.assertj.core.api.Assertions.*;
19+
1820
import java.time.Duration;
21+
import java.util.Optional;
1922
import java.util.concurrent.Callable;
2023
import java.util.function.BiConsumer;
2124
import java.util.function.BiFunction;
@@ -40,8 +43,6 @@
4043
import org.springframework.transaction.support.TransactionTemplate;
4144
import org.springframework.util.Assert;
4245

43-
import com.tngtech.archunit.thirdparty.com.google.common.base.Optional;
44-
4546
/**
4647
* A DSL to define integration testing scenarios for application modules. A {@link Scenario} starts with a stimulus on
4748
* the system, usually a component invocation (see {@link #stimulate(Function)} or event publication (see
@@ -338,13 +339,16 @@ public <S> StateChangeResult<S> forStateChange(Supplier<S> supplier, Predicate<?
338339
}
339340

340341
/**
342+
* Expects an event of the given type to arrive. Use API on the returned {@link EventResult} to specify more
343+
* detailed expectations and conclude those with a call a flavor of {@link EventResult#toArrive()}.
344+
*
341345
* @param <E> the type of the event.
342346
* @param type must not be {@literal null}.
343347
* @return will never be {@literal null}.
344348
* @see #forEventOfType(Class)
345349
*/
346350
public <E> EventResult<E> andWaitForEventOfType(Class<E> type) {
347-
return new EventResult<E>(type, Function.identity());
351+
return new EventResult<E>(type, Function.identity(), null);
348352
}
349353

350354
/**
@@ -402,6 +406,11 @@ private <S> ExecutionResult<S, T> awaitInternal(Consumer<T> verifications, Calla
402406

403407
private record ExecutionResult<S, T>(S first, T second) {}
404408

409+
/**
410+
* The result of an expected state change.
411+
*
412+
* @author Oliver Drotbohm
413+
*/
405414
public class StateChangeResult<S> {
406415

407416
private ExecutionResult<S, T> result;
@@ -445,25 +454,48 @@ public void andVerifyEvents(Consumer<AssertablePublishedEvents> events) {
445454

446455
events.accept(Scenario.this.events);
447456
}
457+
458+
/**
459+
* Expects an event of the given type to arrive eventually. Use API on the returned {@link EventResult} to specify
460+
* more detailed expectations and conclude those with a call a flavor of {@link EventResult#toArrive()}.
461+
*
462+
* @param <E> the type of the event
463+
* @param eventType must not be {@literal null}.
464+
* @return will never be {@literal null}.
465+
*/
466+
public <E> EventResult<E> andExpect(Class<E> eventType) {
467+
return new EventResult<>(eventType, Function.identity(), result);
468+
}
448469
}
449470

471+
/**
472+
* The result of an expected event publication.
473+
*
474+
* @author Oliver Drotbohm
475+
*/
450476
public class EventResult<E> {
451477

478+
private static final String EXPECTED_EVENT = "Expected an event of type %s (potentially further constrained using matching clauses above) to be published but couldn't find one in %s!";
479+
452480
private final Class<E> type;
453481
private final Function<TypedPublishedEvents<E>, TypedPublishedEvents<E>> filter;
482+
private final ExecutionResult<?, T> previousResult;
454483

455484
/**
456485
* Creates a new {@link EventResult} for the given type and filter.
457486
*
458487
* @param type must not be {@literal null}.
459488
* @param filtered must not be {@literal null}.
489+
* @param previousResult a potentially previously calculated result.
460490
*/
461-
EventResult(Class<E> type, Function<TypedPublishedEvents<E>, TypedPublishedEvents<E>> filtered) {
491+
EventResult(Class<E> type, Function<TypedPublishedEvents<E>, TypedPublishedEvents<E>> filtered,
492+
ExecutionResult<?, T> previousResult) {
462493

463494
Assert.notNull(type, "Event type must not be null!");
464495

465496
this.type = type;
466497
this.filter = filtered;
498+
this.previousResult = previousResult;
467499
}
468500

469501
/**
@@ -476,7 +508,7 @@ public EventResult<E> matching(Predicate<? super E> filter) {
476508

477509
Assert.notNull(filter, "Filter must not be null!");
478510

479-
return new EventResult<E>(type, createOrAdd(it -> it.matching(filter)));
511+
return new EventResult<E>(type, createOrAdd(it -> it.matching(filter)), previousResult);
480512
}
481513

482514
/**
@@ -489,7 +521,7 @@ public EventResult<E> matching(Predicate<? super E> filter) {
489521
* @return will never be {@literal null}.
490522
*/
491523
public <S> EventResult<E> matchingMapped(Function<E, S> extractor, Predicate<? super S> filter) {
492-
return new EventResult<E>(type, createOrAdd(it -> it.matching(extractor, filter)));
524+
return new EventResult<E>(type, createOrAdd(it -> it.matching(extractor, filter)), previousResult);
493525
}
494526

495527
/**
@@ -501,7 +533,7 @@ public <S> EventResult<E> matchingMapped(Function<E, S> extractor, Predicate<? s
501533
* @return will never be {@literal null}.
502534
*/
503535
public <S> EventResult<E> matchingMappedValue(Function<E, S> extractor, @Nullable S value) {
504-
return new EventResult<E>(type, createOrAdd(it -> it.matching(extractor, value)));
536+
return new EventResult<E>(type, createOrAdd(it -> it.matching(extractor, value)), previousResult);
505537
}
506538

507539
/**
@@ -584,7 +616,18 @@ private PublishedEventAssert<? super E> getAssertedEvent() {
584616
}
585617

586618
private void toArriveAndVerifyInternal(Consumer<T> verifications) {
587-
awaitInternal(verifications, () -> getFilteredEvents(), it -> it.eventOfTypeWasPublished(type));
619+
620+
if (previousResult != null) {
621+
622+
assertThat(getFilteredEvents().eventOfTypeWasPublished(type))
623+
.overridingErrorMessage(EXPECTED_EVENT, type, events)
624+
.isTrue();
625+
626+
verifications.accept(previousResult.second());
627+
628+
} else {
629+
awaitInternal(verifications, () -> getFilteredEvents(), it -> it.eventOfTypeWasPublished(type));
630+
}
588631
}
589632
}
590633
}

spring-modulith-test/src/test/java/org/springframework/modulith/test/ScenarioUnitTests.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import java.lang.Thread.UncaughtExceptionHandler;
2525
import java.time.Duration;
26+
import java.util.List;
2627
import java.util.function.BiConsumer;
2728
import java.util.function.Consumer;
2829
import java.util.function.Function;
@@ -34,6 +35,7 @@
3435
import org.junit.jupiter.api.extension.ExtendWith;
3536
import org.mockito.Mock;
3637
import org.mockito.junit.jupiter.MockitoExtension;
38+
import org.opentest4j.AssertionFailedError;
3739
import org.springframework.context.ApplicationEventPublisher;
3840
import org.springframework.context.PayloadApplicationEvent;
3941
import org.springframework.modulith.test.PublishedEventsAssert.PublishedEventAssert;
@@ -396,6 +398,41 @@ void invokesEventVerificationOnMethodInvocationStimulus() {
396398
verify(consumer).accept(any());
397399
}
398400

401+
@Test // GH-185
402+
void attachedEventConstraintsAreVerified() {
403+
404+
var runnable = mock(Runnable.class);
405+
var events = new DefaultPublishedEvents(List.of(new SomeEvent("payload")));
406+
var publishedEvents = new DefaultAssertablePublishedEvents(events);
407+
408+
assertThatNoException().isThrownBy(() -> {
409+
new Scenario(tx, publisher, publishedEvents)
410+
.stimulate(runnable)
411+
.andWaitForStateChange(() -> true)
412+
.andExpect(SomeEvent.class)
413+
.matching(it -> it != null)
414+
.toArrive();
415+
});
416+
417+
verify(runnable).run();
418+
}
419+
420+
@Test // GH-185
421+
void failsAttachedEventConstraintsIfNoEventsPublished() {
422+
423+
var runnable = mock(Runnable.class);
424+
425+
assertThatExceptionOfType(AssertionFailedError.class)
426+
.isThrownBy(() -> new Scenario(tx, publisher, new DefaultAssertablePublishedEvents())
427+
.stimulate(runnable)
428+
.andWaitForStateChange(() -> true)
429+
.andExpect(SomeEvent.class)
430+
.matching(it -> it != null)
431+
.toArrive());
432+
433+
verify(runnable).run();
434+
}
435+
399436
private Fixture givenAScenario(Consumer<Scenario> consumer) {
400437
return new Fixture(consumer, DELAY, null, new DefaultAssertablePublishedEvents());
401438
}

0 commit comments

Comments
 (0)