From 5d285e0e034a3ada1cf43d2cc973e6ad64ccc917 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Tue, 3 Jan 2023 02:44:01 +0100 Subject: [PATCH] GH-100 - Introduce AssertablePublishedEvents. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In case AssertJ is on the classpath, test cases using the PublishedEventsExtension (for example, implicitly activated via @ApplicationModuleTest) can get an AssertablePublishedEvents injected into the method. In contrast to the original PublishedEvents, that one acts as AssertProvider so that it can be used in assertThat(…).… expressions to verify application events published. Polished API in PublishedEvents for a more consistent experience. --- .../example/order/OrderIntegrationTests.java | 14 +- .../test/AssertablePublishedEvents.java | 35 +++++ .../DefaultAssertablePublishedEvents.java | 57 ++++++++ .../modulith/test/DefaultPublishedEvents.java | 23 +-- .../modulith/test/PublishedEvents.java | 44 +++++- .../modulith/test/PublishedEventsAssert.java | 138 ++++++++++++++++++ .../PublishedEventsParameterResolver.java | 19 ++- ...ishedEventsParameterResolverUnitTests.java | 5 +- 8 files changed, 316 insertions(+), 19 deletions(-) create mode 100644 spring-modulith-test/src/main/java/org/springframework/modulith/test/AssertablePublishedEvents.java create mode 100644 spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultAssertablePublishedEvents.java create mode 100644 spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEventsAssert.java diff --git a/spring-modulith-example/src/test/java/example/order/OrderIntegrationTests.java b/spring-modulith-example/src/test/java/example/order/OrderIntegrationTests.java index 2c7f9b73..04e8deb6 100644 --- a/spring-modulith-example/src/test/java/example/order/OrderIntegrationTests.java +++ b/spring-modulith-example/src/test/java/example/order/OrderIntegrationTests.java @@ -21,7 +21,7 @@ import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.Test; import org.springframework.modulith.test.ApplicationModuleTest; -import org.springframework.modulith.test.PublishedEvents; +import org.springframework.modulith.test.AssertablePublishedEvents; /** * @author Oliver Drotbohm @@ -33,15 +33,23 @@ class OrderIntegrationTests { private final OrderManagement orders; @Test - void publishesOrderCompletion(PublishedEvents events) { + void publishesOrderCompletion(AssertablePublishedEvents events) { var reference = new Order(); orders.complete(reference); + // Verification + var matchingMapped = events.ofType(OrderCompleted.class) - .matchingMapped(OrderCompleted::getOrderId, reference.getId()::equals); + .matching(OrderCompleted::getOrderId, reference.getId()); assertThat(matchingMapped).hasSize(1); + + // AssertJ + + assertThat(events) + .contains(OrderCompleted.class) + .matching(OrderCompleted::getOrderId, reference.getId()); } } diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/AssertablePublishedEvents.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/AssertablePublishedEvents.java new file mode 100644 index 00000000..b5c80c8a --- /dev/null +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/AssertablePublishedEvents.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022-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.assertj.core.api.AssertProvider; + +/** + * An AssertJ-based extension of {@link PublishedEvents} to obtain fluent assertions. + * + * @author Oliver Drotbohm + */ +public interface AssertablePublishedEvents extends PublishedEvents, AssertProvider { + + /* + * (non-Javadoc) + * @see org.assertj.core.api.AssertProvider#assertThat() + */ + @Override + public default PublishedEventsAssert assertThat() { + return new PublishedEventsAssert(this); + } +} diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultAssertablePublishedEvents.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultAssertablePublishedEvents.java new file mode 100644 index 00000000..a7692fa3 --- /dev/null +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultAssertablePublishedEvents.java @@ -0,0 +1,57 @@ +/* + * 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 lombok.RequiredArgsConstructor; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; + +/** + * Default implementation of {@link AssertablePublishedEvents}. + * + * @author Oliver Drotbohm + */ +@RequiredArgsConstructor +class DefaultAssertablePublishedEvents implements AssertablePublishedEvents, ApplicationListener { + + private final DefaultPublishedEvents delegate; + + /** + * Creates a new {@link DefaultAssertablePublishedEvents}. + */ + DefaultAssertablePublishedEvents() { + this(new DefaultPublishedEvents()); + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.test.PublishedEvents#ofType(java.lang.Class) + */ + @Override + public TypedPublishedEvents ofType(Class type) { + return delegate.ofType(type); + } + + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) + */ + @Override + public void onApplicationEvent(ApplicationEvent event) { + delegate.onApplicationEvent(event); + } +} diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultPublishedEvents.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultPublishedEvents.java index 73751f34..8f182414 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultPublishedEvents.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/DefaultPublishedEvents.java @@ -22,9 +22,9 @@ import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.function.Function; import java.util.function.Predicate; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.context.ApplicationEvent; @@ -106,7 +106,6 @@ class DefaultPublishedEvents implements PublishedEvents, ApplicationListener TypedPublishedEvents matchingMapped(Function mapper, Predicate predicate) { + public TypedPublishedEvents matching(Function mapper, Predicate predicate) { + return SimpleTypedPublishedEvents.of(events.stream().filter(it -> predicate.test(mapper.apply(it)))); + } - return SimpleTypedPublishedEvents.of(events.stream().flatMap(it -> { - - S mapped = mapper.apply(it); - - return predicate.test(mapped) ? Stream.of(it) : Stream.empty(); - - })); + /* + * (non-Javadoc) + * @see org.springframework.modulith.test.PublishedEvents.TypedPublishedEvents#matching(java.util.function.Function, java.lang.Object) + */ + @Override + public TypedPublishedEvents matching(Function mapper, S value) { + return matching(mapper, (Predicate) it -> Objects.equals(it, value)); } /** diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEvents.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEvents.java index 1170c643..f0df2895 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEvents.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEvents.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.function.Function; import java.util.function.Predicate; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -61,6 +62,19 @@ public interface PublishedEvents { */ TypedPublishedEvents ofType(Class 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. * @@ -91,9 +105,35 @@ public interface PublishedEvents { * * @param the intermediate type to apply the {@link Predicate} on * @param mapper the mapping step to extract a part of the original event subject to test for the {@link Predicate}. - * @param predicate the {@link Predicate} to apply on the value extracted. + * Must not be {@literal null}. + * @param predicate the {@link Predicate} to apply on the value extracted. Must not be {@literal null}. * @return will never be {@literal null}. */ - TypedPublishedEvents matchingMapped(Function mapper, Predicate predicate); + TypedPublishedEvents matching(Function mapper, Predicate predicate); + + /** + * Returns all {@link TypedPublishedEvents} that match the given value after applying the given mapping step. + * + * @param the intermediate type to apply the {@link Predicate} on + * @param mapper the mapping step to extract a part of the original event subject to verify against the given value, + * must not be {@literal null}. + * @param value the value expected as outcome of the mapping step, can be {@literal null}. + * @return will never be {@literal null}. + */ + TypedPublishedEvents matching(Function mapper, @Nullable S value); + + /** + * Returns all {@link TypedPublishedEvents} that match the given predicate after applying the given mapping step. + * + * @param the intermediate type to apply the {@link Predicate} on + * @param mapper the mapping step to extract a part of the original event subject to test for the {@link Predicate}. + * @param predicate the {@link Predicate} to apply on the value extracted. + * @return will never be {@literal null}. + * @deprecated since 0.3, use {@link #matching(Function, Predicate)} instead. + */ + @Deprecated(forRemoval = true, since = "0.3") + default TypedPublishedEvents matchingMapped(Function mapper, Predicate predicate) { + return matching(mapper, predicate); + } } } diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEventsAssert.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEventsAssert.java new file mode 100644 index 00000000..7682e3ca --- /dev/null +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEventsAssert.java @@ -0,0 +1,138 @@ +/* + * Copyright 2022-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 lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +import java.util.function.Function; +import java.util.function.Predicate; + +import org.assertj.core.api.AbstractAssert; +import org.springframework.lang.Nullable; +import org.springframework.modulith.test.PublishedEvents.TypedPublishedEvents; +import org.springframework.util.Assert; + +/** + * AssertJ {@link org.assertj.core.api.Assert} for {@link PublishedEvents}. + * + * @author Oliver Drotbohm + * @see AssertablePublishedEvents + */ +public class PublishedEventsAssert extends AbstractAssert { + + /** + * Creates a new {@link PublishedEventsAssert} + * + * @param actual must not be {@literal null}. + */ + PublishedEventsAssert(AssertablePublishedEvents actual) { + super(actual, PublishedEventsAssert.class); + } + + /** + * Asserts that the {@link PublishedEvents} contain at least one event of the given type. Use the returned + * {@link PublishedEventsAssert} to further qualify the assertions. + * + * @param the event type expected + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ + public PublishedEventAssert contains(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + var ofType = actual.ofType(type); + + assertThat(ofType).isNotEmpty(); + + return new PublishedEventAssert<>(ofType); + } + + /** + * Assertions further qualifying the expected events. + * + * @author Oliver Drotbohm + */ + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + public class PublishedEventAssert { + + private final TypedPublishedEvents events; + + /** + * Asserts that at least one event matches the given predicate. + * + * @param predicate must not be {@literal null}. + * @return will never be {@literal null}. + */ + public PublishedEventAssert matching(Predicate predicate) { + + Assert.notNull(predicate, "Predicate must not be null!"); + + assertThat(events.matching(predicate)).isNotEmpty(); + + return this; + } + + /** + * Asserts that at least one event exists for which the value extracted by the given {@link Function} matches the + * given {@link Predicate}. + * + * @param the type of the value to be matched. + * @param function the extractor function, must not be {@literal null}. + * @param predicate the {@link Predicate} the extracted value is supposed to match. Must not be {@literal null}. + * @return will never be {@literal null}. + */ + public PublishedEventAssert matching(Function function, Predicate predicate) { + + Assert.notNull(function, "Function must not be null!"); + Assert.notNull(predicate, "Predicate must not be null!"); + + assertThat(events.matching(function, predicate)).isNotEmpty(); + + return this; + } + + /** + * Asserts that at least one event exists for which the value extracted by the given {@link Function} matches the + * given one. + * + * @param the type of the value to be matched + * @param function the extractor function, must not be {@literal null}. + * @param value the expected value, can be {@literal null}. + * @return will never be {@literal null}. + */ + public PublishedEventAssert matching(Function function, @Nullable S value) { + + Assert.notNull(function, "Function must not be null!"); + + assertThat(events.matching(function, value)).isNotEmpty(); + + return this; + } + + /** + * Syntactic sugar to start a new assertion on a different type of event. + * + * @return will never be {@literal null}. + */ + public PublishedEventsAssert and() { + return PublishedEventsAssert.this; + } + } +} diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEventsParameterResolver.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEventsParameterResolver.java index 1c12cbe1..2a38f101 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEventsParameterResolver.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEventsParameterResolver.java @@ -28,6 +28,7 @@ import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * Provides instances of {@link PublishedEvents} as test method parameters. @@ -36,6 +37,9 @@ import org.springframework.util.Assert; */ class PublishedEventsParameterResolver implements ParameterResolver, BeforeAllCallback, AfterEachCallback { + private static final boolean ASSERT_J_PRESENT = ClassUtils.isPresent("org.assertj.core.api.Assert", + PublishedEventsParameterResolver.class.getClassLoader()); + private ThreadBoundApplicationListenerAdapter listener = new ThreadBoundApplicationListenerAdapter(); private final Function lookup; @@ -64,7 +68,15 @@ class PublishedEventsParameterResolver implements ParameterResolver, BeforeAllCa */ @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { - return PublishedEvents.class.isAssignableFrom(parameterContext.getParameter().getType()); + + var type = parameterContext.getParameter().getType(); + + if (type.getName().equals("org.springframework.modulith.test.AssertablePublishedEvents") && !ASSERT_J_PRESENT) { + throw new IllegalStateException( + "Method declares AssertablePublishedEvents as parameter but AssertJ is not on the classpath!"); + } + + return PublishedEvents.class.isAssignableFrom(type); } /* @@ -74,7 +86,10 @@ class PublishedEventsParameterResolver implements ParameterResolver, BeforeAllCa @Override public PublishedEvents resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { - DefaultPublishedEvents publishedEvents = new DefaultPublishedEvents(); + var publishedEvents = ASSERT_J_PRESENT + ? new DefaultAssertablePublishedEvents() + : new DefaultPublishedEvents(); + listener.registerDelegate(publishedEvents); return publishedEvents; diff --git a/spring-modulith-test/src/test/java/org/springframework/modulith/test/PublishedEventsParameterResolverUnitTests.java b/spring-modulith-test/src/test/java/org/springframework/modulith/test/PublishedEventsParameterResolverUnitTests.java index 1c89acb7..ec727de9 100644 --- a/spring-modulith-test/src/test/java/org/springframework/modulith/test/PublishedEventsParameterResolverUnitTests.java +++ b/spring-modulith-test/src/test/java/org/springframework/modulith/test/PublishedEventsParameterResolverUnitTests.java @@ -36,7 +36,7 @@ import org.springframework.util.ReflectionUtils; * * @author Oliver Drotbohm */ -public class PublishedEventsParameterResolverUnitTests { +class PublishedEventsParameterResolverUnitTests { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); @@ -46,6 +46,7 @@ public class PublishedEventsParameterResolverUnitTests { PublishedEventsParameterResolver resolver = new PublishedEventsParameterResolver(__ -> context); assertThat(resolver.supportsParameter(getParameterContext(PublishedEvents.class), null)).isTrue(); + assertThat(resolver.supportsParameter(getParameterContext(AssertablePublishedEvents.class), null)).isTrue(); assertThat(resolver.supportsParameter(getParameterContext(Object.class), null)).isFalse(); } @@ -99,5 +100,7 @@ public class PublishedEventsParameterResolverUnitTests { void with(PublishedEvents events); void with(Object object); + + void with(AssertablePublishedEvents events); } }