GH-100 - Introduce AssertablePublishedEvents.
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.
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PublishedEventsAssert> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.assertj.core.api.AssertProvider#assertThat()
|
||||
*/
|
||||
@Override
|
||||
public default PublishedEventsAssert assertThat() {
|
||||
return new PublishedEventsAssert(this);
|
||||
}
|
||||
}
|
||||
@@ -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<ApplicationEvent> {
|
||||
|
||||
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 <T> TypedPublishedEvents<T> ofType(Class<T> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<App
|
||||
|
||||
return SimpleTypedPublishedEvents.of(getFilteredEvents(subType::isInstance) //
|
||||
.map(subType::cast));
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -120,18 +119,20 @@ class DefaultPublishedEvents implements PublishedEvents, ApplicationListener<App
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.test.PublishedEvents.TypedPublishedEvents#matchingMapped(java.util.function.Function, java.util.function.Predicate)
|
||||
* @see org.springframework.modulith.test.PublishedEvents.TypedPublishedEvents#matching(java.util.function.Function, java.util.function.Predicate)
|
||||
*/
|
||||
@Override
|
||||
public <S> TypedPublishedEvents<T> matchingMapped(Function<T, S> mapper, Predicate<? super S> predicate) {
|
||||
public <S> TypedPublishedEvents<T> matching(Function<T, S> mapper, Predicate<? super S> 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 <S> TypedPublishedEvents<T> matching(Function<T, S> mapper, S value) {
|
||||
return matching(mapper, (Predicate<S>) it -> Objects.equals(it, value));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
*/
|
||||
<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.
|
||||
*
|
||||
@@ -91,9 +105,35 @@ public interface PublishedEvents {
|
||||
*
|
||||
* @param <S> 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}.
|
||||
*/
|
||||
<S> TypedPublishedEvents<T> matchingMapped(Function<T, S> mapper, Predicate<? super S> predicate);
|
||||
<S> TypedPublishedEvents<T> matching(Function<T, S> mapper, Predicate<? super S> predicate);
|
||||
|
||||
/**
|
||||
* Returns all {@link TypedPublishedEvents} that match the given value after applying the given mapping step.
|
||||
*
|
||||
* @param <S> 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}.
|
||||
*/
|
||||
<S> TypedPublishedEvents<T> matching(Function<T, S> mapper, @Nullable S value);
|
||||
|
||||
/**
|
||||
* Returns all {@link TypedPublishedEvents} that match the given predicate after applying the given mapping step.
|
||||
*
|
||||
* @param <S> 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 <S> TypedPublishedEvents<T> matchingMapped(Function<T, S> mapper, Predicate<? super S> predicate) {
|
||||
return matching(mapper, predicate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PublishedEventsAssert, PublishedEvents> {
|
||||
|
||||
/**
|
||||
* 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 <T> the event type expected
|
||||
* @param type must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public <T> PublishedEventAssert<T> contains(Class<T> 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<T> {
|
||||
|
||||
private final TypedPublishedEvents<T> 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<T> matching(Predicate<? super T> 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 <S> 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 <S> PublishedEventAssert<T> matching(Function<T, S> function, Predicate<? super S> 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 <S> 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 <S> PublishedEventAssert<T> matching(Function<T, S> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ExtensionContext, ApplicationContext> 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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user