> expected = asList(ServletTestExecutionListener.class,//
+ DirtiesContextBeforeModesTestExecutionListener.class,//
+ ApplicationEventsTestExecutionListener.class,//
+ DependencyInjectionTestExecutionListener.class,//
+ BarTestExecutionListener.class,//
+ DirtiesContextTestExecutionListener.class,//
+ TransactionalTestExecutionListener.class,//
+ SqlScriptsTestExecutionListener.class,//
+ EventPublishingTestExecutionListener.class
+ );
assertRegisteredListeners(MergedDefaultListenersWithCustomListenerInsertedTestCase.class, expected);
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/DefaultPublishedEvents.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/DefaultPublishedEvents.java
new file mode 100644
index 0000000000..5d3b67f658
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/DefaultPublishedEvents.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2002-2020 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.test.context.junit.jupiter.event;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.springframework.test.context.event.ApplicationEventsHolder;
+
+/**
+ * Default implementation of {@link PublishedEvents}.
+ *
+ * Copied from the Moduliths project.
+ *
+ * @author Oliver Drotbohm
+ * @author Sam Brannen
+ * @since 5.3.3
+ */
+class DefaultPublishedEvents implements PublishedEvents {
+
+ @Override
+ public TypedPublishedEvents ofType(Class type) {
+ return SimpleTypedPublishedEvents.of(ApplicationEventsHolder.getRequiredApplicationEvents().stream(type));
+ }
+
+
+ private static class SimpleTypedPublishedEvents implements TypedPublishedEvents {
+
+ private final List events;
+
+ private SimpleTypedPublishedEvents(List events) {
+ this.events = events;
+ }
+
+ static SimpleTypedPublishedEvents of(Stream stream) {
+ return new SimpleTypedPublishedEvents<>(stream.collect(Collectors.toList()));
+ }
+
+ @Override
+ public TypedPublishedEvents ofSubType(Class subType) {
+ return SimpleTypedPublishedEvents.of(getFilteredEvents(subType::isInstance)//
+ .map(subType::cast));
+ }
+
+ @Override
+ public TypedPublishedEvents matching(Predicate super T> predicate) {
+ return SimpleTypedPublishedEvents.of(getFilteredEvents(predicate));
+ }
+
+ @Override
+ public TypedPublishedEvents matchingMapped(Function mapper, Predicate super S> predicate) {
+ return SimpleTypedPublishedEvents.of(this.events.stream().flatMap(it -> {
+ S mapped = mapper.apply(it);
+ return predicate.test(mapped) ? Stream.of(it) : Stream.empty();
+ }));
+ }
+
+ private Stream getFilteredEvents(Predicate super T> predicate) {
+ return this.events.stream().filter(predicate);
+ }
+
+ @Override
+ public Iterator iterator() {
+ return this.events.iterator();
+ }
+
+ @Override
+ public String toString() {
+ return this.events.toString();
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/JUnitJupiterApplicationEventsIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/JUnitJupiterApplicationEventsIntegrationTests.java
new file mode 100644
index 0000000000..5e5ee6921c
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/JUnitJupiterApplicationEventsIntegrationTests.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2002-2020 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.test.context.junit.jupiter.event;
+
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.TestInstance;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.event.ApplicationEvents;
+import org.springframework.test.context.event.RecordApplicationEvents;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
+import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD;
+
+/**
+ * Integration tests for {@link ApplicationEvents} in conjunction with JUnit Jupiter.
+ *
+ * @author Sam Brannen
+ * @since 5.3.3
+ */
+@SpringJUnitConfig
+@RecordApplicationEvents
+class JUnitJupiterApplicationEventsIntegrationTests {
+
+ @Autowired
+ ApplicationContext context;
+
+ @Autowired
+ ApplicationEvents applicationEvents;
+
+
+ @Nested
+ @TestInstance(PER_METHOD)
+ class TestInstancePerMethodTests {
+
+ @BeforeEach
+ void beforeEach() {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent");
+ context.publishEvent(new CustomEvent("beforeEach"));
+ assertCustomEvents(applicationEvents, "beforeEach");
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent");
+ }
+
+ @Test
+ void test1(ApplicationEvents events, TestInfo testInfo) {
+ assertTestExpectations(events, testInfo);
+ }
+
+ @Test
+ void test2(@Autowired ApplicationEvents events, TestInfo testInfo) {
+ assertTestExpectations(events, testInfo);
+ }
+
+ private void assertTestExpectations(ApplicationEvents events, TestInfo testInfo) {
+ String testName = testInfo.getTestMethod().get().getName();
+
+ assertEventTypes(events, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
+ "BeforeTestExecutionEvent");
+ context.publishEvent(new CustomEvent(testName));
+ context.publishEvent("payload1");
+ context.publishEvent("payload2");
+ assertCustomEvents(events, "beforeEach", testName);
+ assertPayloads(events.stream(String.class), "payload1", "payload2");
+ }
+
+ @AfterEach
+ void afterEach(@Autowired ApplicationEvents events, TestInfo testInfo) {
+ String testName = testInfo.getTestMethod().get().getName();
+
+ assertEventTypes(events, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
+ "BeforeTestExecutionEvent", "CustomEvent", "PayloadApplicationEvent", "PayloadApplicationEvent",
+ "AfterTestExecutionEvent");
+ context.publishEvent(new CustomEvent("afterEach"));
+ assertCustomEvents(events, "beforeEach", testName, "afterEach");
+ assertEventTypes(events, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
+ "BeforeTestExecutionEvent", "CustomEvent", "PayloadApplicationEvent", "PayloadApplicationEvent",
+ "AfterTestExecutionEvent", "CustomEvent");
+ }
+ }
+
+ @Nested
+ @TestInstance(PER_METHOD)
+ class TestInstancePerMethodWithClearedEventsTests {
+
+ @BeforeEach
+ void beforeEach() {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent");
+ context.publishEvent(new CustomEvent("beforeEach"));
+ assertCustomEvents(applicationEvents, "beforeEach");
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent");
+ applicationEvents.clear();
+ assertThat(applicationEvents.stream()).isEmpty();
+ }
+
+ @Test
+ void test1(ApplicationEvents events, TestInfo testInfo) {
+ assertTestExpectations(events, testInfo);
+ }
+
+ @Test
+ void test2(@Autowired ApplicationEvents events, TestInfo testInfo) {
+ assertTestExpectations(events, testInfo);
+ }
+
+ private void assertTestExpectations(ApplicationEvents events, TestInfo testInfo) {
+ String testName = testInfo.getTestMethod().get().getName();
+
+ assertEventTypes(events, "BeforeTestExecutionEvent");
+ context.publishEvent(new CustomEvent(testName));
+ assertCustomEvents(events, testName);
+ assertEventTypes(events, "BeforeTestExecutionEvent", "CustomEvent");
+ }
+
+ @AfterEach
+ void afterEach(@Autowired ApplicationEvents events, TestInfo testInfo) {
+ events.clear();
+ context.publishEvent(new CustomEvent("afterEach"));
+ assertCustomEvents(events, "afterEach");
+ assertEventTypes(events, "CustomEvent");
+ }
+ }
+
+ @Nested
+ @TestInstance(PER_CLASS)
+ class TestInstancePerClassTests {
+
+ private boolean testAlreadyExecuted = false;
+
+ @BeforeEach
+ void beforeEach(TestInfo testInfo) {
+ if (!testAlreadyExecuted) {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestClassEvent",
+ "BeforeTestMethodEvent");
+ }
+ else {
+ assertEventTypes(applicationEvents, "BeforeTestMethodEvent");
+ }
+
+ context.publishEvent(new CustomEvent("beforeEach"));
+ assertCustomEvents(applicationEvents, "beforeEach");
+
+ if (!testAlreadyExecuted) {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestClassEvent",
+ "BeforeTestMethodEvent", "CustomEvent");
+ }
+ else {
+ assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent");
+ }
+ }
+
+ @Test
+ void test1(ApplicationEvents events, TestInfo testInfo) {
+ assertTestExpectations(events, testInfo);
+ }
+
+ @Test
+ void test2(@Autowired ApplicationEvents events, TestInfo testInfo) {
+ assertTestExpectations(events, testInfo);
+ }
+
+ private void assertTestExpectations(ApplicationEvents events, TestInfo testInfo) {
+ String testName = testInfo.getTestMethod().get().getName();
+
+ if (!testAlreadyExecuted) {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestClassEvent",
+ "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent");
+ }
+ else {
+ assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent");
+ }
+
+ context.publishEvent(new CustomEvent(testName));
+ assertCustomEvents(events, "beforeEach", testName);
+
+ if (!testAlreadyExecuted) {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestClassEvent",
+ "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent", "CustomEvent");
+ }
+ else {
+ assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent",
+ "CustomEvent");
+ }
+ }
+
+ @AfterEach
+ void afterEach(@Autowired ApplicationEvents events, TestInfo testInfo) {
+ String testName = testInfo.getTestMethod().get().getName();
+
+ if (!testAlreadyExecuted) {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestClassEvent",
+ "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent", "CustomEvent",
+ "AfterTestExecutionEvent");
+ }
+ else {
+ assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent",
+ "CustomEvent", "AfterTestExecutionEvent");
+ }
+
+ context.publishEvent(new CustomEvent("afterEach"));
+ assertCustomEvents(events, "beforeEach", testName, "afterEach");
+
+ if (!testAlreadyExecuted) {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestClassEvent",
+ "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent", "CustomEvent",
+ "AfterTestExecutionEvent", "CustomEvent");
+ testAlreadyExecuted = true;
+ }
+ else {
+ assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent",
+ "CustomEvent", "AfterTestExecutionEvent", "CustomEvent");
+ }
+ }
+ }
+
+
+ private static void assertEventTypes(ApplicationEvents applicationEvents, String... types) {
+ assertThat(applicationEvents.stream().map(event -> event.getClass().getSimpleName()))
+ .containsExactly(types);
+ }
+
+ private static void assertPayloads(Stream events, String... values) {
+ assertThat(events).extracting(Object::toString).containsExactly(values);
+ }
+
+ private static void assertCustomEvents(ApplicationEvents events, String... messages) {
+ assertThat(events.stream(CustomEvent.class)).extracting(CustomEvent::getMessage).containsExactly(messages);
+ }
+
+
+ @Configuration
+ static class Config {
+ }
+
+ @SuppressWarnings("serial")
+ static class CustomEvent extends ApplicationEvent {
+
+ private final String message;
+
+
+ CustomEvent(String message) {
+ super(message);
+ this.message = message;
+ }
+
+ String getMessage() {
+ return message;
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/ParallelApplicationEventsIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/ParallelApplicationEventsIntegrationTests.java
new file mode 100644
index 0000000000..d04fb230f1
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/ParallelApplicationEventsIntegrationTests.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2002-2020 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.test.context.junit.jupiter.event;
+
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.junit.platform.testkit.engine.EngineTestKit;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.event.ApplicationEvents;
+import org.springframework.test.context.event.RecordApplicationEvents;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+
+/**
+ * Integration tests that verify parallel execution support for {@link ApplicationEvents}
+ * in conjunction with JUnit Jupiter.
+ *
+ * @author Sam Brannen
+ * @since 5.3.3
+ */
+class ParallelApplicationEventsIntegrationTests {
+
+ private static final Set payloads = ConcurrentHashMap.newKeySet();
+
+
+ @ParameterizedTest
+ @ValueSource(classes = {TestInstancePerMethodTestCase.class, TestInstancePerClassTestCase.class})
+ void executeTestsInParallel(Class> testClass) {
+ EngineTestKit.engine("junit-jupiter")//
+ .selectors(selectClass(testClass))//
+ .configurationParameter("junit.jupiter.execution.parallel.enabled", "true")//
+ .configurationParameter("junit.jupiter.execution.parallel.config.dynamic.factor", "10")//
+ .execute()//
+ .testEvents()//
+ .assertStatistics(stats -> stats.started(10).succeeded(10).failed(0));
+
+ Set testNames = payloads.stream()//
+ .map(payload -> payload.substring(0, payload.indexOf("-")))//
+ .collect(Collectors.toSet());
+ Set threadNames = payloads.stream()//
+ .map(payload -> payload.substring(payload.indexOf("-")))//
+ .collect(Collectors.toSet());
+
+ assertThat(payloads).hasSize(10);
+ assertThat(testNames).hasSize(10);
+ // There are probably 10 different thread names, but we really just want
+ // to assert that at least a few different threads were used.
+ assertThat(threadNames).hasSizeGreaterThanOrEqualTo(4);
+ }
+
+
+ @AfterEach
+ void resetPayloads() {
+ payloads.clear();
+ }
+
+
+ @SpringJUnitConfig
+ @RecordApplicationEvents
+ @Execution(ExecutionMode.CONCURRENT)
+ @TestInstance(Lifecycle.PER_METHOD)
+ static class TestInstancePerMethodTestCase {
+
+ @Autowired
+ ApplicationContext context;
+
+ @Autowired
+ ApplicationEvents events;
+
+ @Test
+ void test1(TestInfo testInfo) {
+ assertTestExpectations(this.events, testInfo);
+ }
+
+ @Test
+ void test2(ApplicationEvents events, TestInfo testInfo) {
+ assertTestExpectations(events, testInfo);
+ }
+
+ @Test
+ void test3(TestInfo testInfo) {
+ assertTestExpectations(this.events, testInfo);
+ }
+
+ @Test
+ void test4(ApplicationEvents events, TestInfo testInfo) {
+ assertTestExpectations(events, testInfo);
+ }
+
+ @Test
+ void test5(TestInfo testInfo) {
+ assertTestExpectations(this.events, testInfo);
+ }
+
+ @Test
+ void test6(ApplicationEvents events, TestInfo testInfo) {
+ assertTestExpectations(events, testInfo);
+ }
+
+ @Test
+ void test7(TestInfo testInfo) {
+ assertTestExpectations(this.events, testInfo);
+ }
+
+ @Test
+ void test8(ApplicationEvents events, TestInfo testInfo) {
+ assertTestExpectations(events, testInfo);
+ }
+
+ @Test
+ void test9(TestInfo testInfo) {
+ assertTestExpectations(this.events, testInfo);
+ }
+
+ @Test
+ void test10(ApplicationEvents events, TestInfo testInfo) {
+ assertTestExpectations(events, testInfo);
+ }
+
+ private void assertTestExpectations(ApplicationEvents events, TestInfo testInfo) {
+ String testName = testInfo.getTestMethod().get().getName();
+ String threadName = Thread.currentThread().getName();
+ String localPayload = testName + "-" + threadName;
+ context.publishEvent(localPayload);
+ assertPayloads(events.stream(String.class), localPayload);
+ }
+
+ private static void assertPayloads(Stream events, String... values) {
+ assertThat(events.peek(payloads::add)).extracting(Object::toString).containsExactly(values);
+ }
+
+ @Configuration
+ static class Config {
+ }
+
+ }
+
+ @TestInstance(Lifecycle.PER_CLASS)
+ static class TestInstancePerClassTestCase extends TestInstancePerMethodTestCase {
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/PublishedEvents.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/PublishedEvents.java
new file mode 100644
index 0000000000..b5e43aac1e
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/PublishedEvents.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2002-2020 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.test.context.junit.jupiter.event;
+
+import java.util.Arrays;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * All Spring application events fired during the test execution.
+ *
+ * Copied from the Moduliths project.
+ *
+ * @author Oliver Drotbohm
+ * @since 5.3.3
+ */
+public interface PublishedEvents {
+
+ /**
+ * Creates a new {@link PublishedEvents} instance for the given events.
+ *
+ * @param events must not be {@literal null}
+ * @return will never be {@literal null}
+ */
+ public static PublishedEvents of(Object... events) {
+ return of(Arrays.asList(events));
+ }
+
+ /**
+ * Returns all application events of the given type that were fired during the test execution.
+ *
+ * @param the event type
+ * @param type must not be {@literal null}
+ */
+ TypedPublishedEvents ofType(Class type);
+
+ /**
+ * All application events of a given type that were fired during a test execution.
+ *
+ * @param the event type
+ */
+ interface TypedPublishedEvents extends Iterable {
+
+ /**
+ * Further constrain the event type for downstream assertions.
+ *
+ * @param subType the sub type
+ * @return will never be {@literal null}
+ */
+ TypedPublishedEvents ofSubType(Class subType);
+
+ /**
+ * Returns all {@link TypedPublishedEvents} that match the given predicate.
+ *
+ * @param predicate must not be {@literal null}
+ * @return will never be {@literal null}
+ */
+ TypedPublishedEvents matching(Predicate super T> predicate);
+
+ /**
+ * 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}
+ */
+ TypedPublishedEvents matchingMapped(Function mapper, Predicate super S> predicate);
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/PublishedEventsExtension.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/PublishedEventsExtension.java
new file mode 100644
index 0000000000..a849bc7e56
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/PublishedEventsExtension.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2002-2020 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.test.context.junit.jupiter.event;
+
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolver;
+
+/**
+ * @author Sam Brannen
+ * @since 5.3.3
+ */
+public class PublishedEventsExtension implements ParameterResolver {
+
+ @Override
+ public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
+ return PublishedEvents.class.isAssignableFrom(parameterContext.getParameter().getType());
+ }
+
+ @Override
+ public PublishedEvents resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
+ return new DefaultPublishedEvents();
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/PublishedEventsIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/PublishedEventsIntegrationTests.java
new file mode 100644
index 0000000000..9f61b8e640
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/PublishedEventsIntegrationTests.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2002-2020 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.test.context.junit.jupiter.event;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.event.ApplicationEvents;
+import org.springframework.test.context.event.BeforeTestExecutionEvent;
+import org.springframework.test.context.event.BeforeTestMethodEvent;
+import org.springframework.test.context.event.PrepareTestInstanceEvent;
+import org.springframework.test.context.event.RecordApplicationEvents;
+import org.springframework.test.context.event.TestContextEvent;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration tests for the example {@link PublishedEvents} extension to the
+ * {@link ApplicationEvents} feature.
+ *
+ * @author Sam Brannen
+ * @since 5.3.3
+ */
+@SpringJUnitConfig
+@RecordApplicationEvents
+@ExtendWith(PublishedEventsExtension.class)
+class PublishedEventsIntegrationTests {
+
+ @Test
+ void test(PublishedEvents publishedEvents) {
+ assertThat(publishedEvents).isNotNull();
+ assertThat(publishedEvents.ofType(TestContextEvent.class)).hasSize(3);
+ assertThat(publishedEvents.ofType(PrepareTestInstanceEvent.class)).hasSize(1);
+ assertThat(publishedEvents.ofType(BeforeTestMethodEvent.class)).hasSize(1);
+ assertThat(publishedEvents.ofType(BeforeTestExecutionEvent.class)).hasSize(1);
+ assertThat(publishedEvents.ofType(TestContextEvent.class).ofSubType(BeforeTestExecutionEvent.class)).hasSize(1);
+ }
+
+
+ @Configuration
+ static class Config {
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/event/JUnit4ApplicationEventsIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/event/JUnit4ApplicationEventsIntegrationTests.java
new file mode 100644
index 0000000000..b5622f6487
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/junit4/event/JUnit4ApplicationEventsIntegrationTests.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2002-2020 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.test.context.junit4.event;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.event.ApplicationEvents;
+import org.springframework.test.context.event.RecordApplicationEvents;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration tests for {@link ApplicationEvents} in conjunction with JUnit 4.
+ *
+ * @author Sam Brannen
+ * @since 5.3.3
+ */
+@RunWith(SpringRunner.class)
+@RecordApplicationEvents
+public class JUnit4ApplicationEventsIntegrationTests {
+
+ @Rule
+ public final TestName testName = new TestName();
+
+ @Autowired
+ ApplicationContext context;
+
+ @Autowired
+ ApplicationEvents applicationEvents;
+
+
+ @Before
+ public void beforeEach() {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent");
+ context.publishEvent(new CustomEvent("beforeEach"));
+ assertThat(applicationEvents.stream(CustomEvent.class)).extracting(CustomEvent::getMessage)//
+ .containsExactly("beforeEach");
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent");
+ }
+
+ @Test
+ public void test1() {
+ assertTestExpectations("test1");
+ }
+
+ @Test
+ public void test2() {
+ assertTestExpectations("test2");
+ }
+
+ private void assertTestExpectations(String testName) {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
+ "BeforeTestExecutionEvent");
+ context.publishEvent(new CustomEvent(testName));
+ assertThat(applicationEvents.stream(CustomEvent.class)).extracting(CustomEvent::getMessage)//
+ .containsExactly("beforeEach", testName);
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
+ "BeforeTestExecutionEvent", "CustomEvent");
+ }
+
+ @After
+ public void afterEach() {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
+ "BeforeTestExecutionEvent", "CustomEvent", "AfterTestExecutionEvent");
+ context.publishEvent(new CustomEvent("afterEach"));
+ assertThat(applicationEvents.stream(CustomEvent.class)).extracting(CustomEvent::getMessage)//
+ .containsExactly("beforeEach", this.testName.getMethodName(), "afterEach");
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
+ "BeforeTestExecutionEvent", "CustomEvent", "AfterTestExecutionEvent", "CustomEvent");
+ }
+
+
+ private static void assertEventTypes(ApplicationEvents applicationEvents, String... types) {
+ assertThat(applicationEvents.stream().map(event -> event.getClass().getSimpleName()))
+ .containsExactly(types);
+ }
+
+
+ @Configuration
+ static class Config {
+ }
+
+ @SuppressWarnings("serial")
+ static class CustomEvent extends ApplicationEvent {
+
+ private final String message;
+
+
+ CustomEvent(String message) {
+ super(message);
+ this.message = message;
+ }
+
+ String getMessage() {
+ return message;
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/testng/event/TestNGApplicationEventsIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/testng/event/TestNGApplicationEventsIntegrationTests.java
new file mode 100644
index 0000000000..e7f32e5ebc
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/testng/event/TestNGApplicationEventsIntegrationTests.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2002-2020 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.test.context.testng.event;
+
+import java.lang.reflect.Method;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.event.ApplicationEvents;
+import org.springframework.test.context.event.RecordApplicationEvents;
+import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration tests for {@link ApplicationEvents} in conjunction with TestNG.
+ *
+ * @author Sam Brannen
+ * @since 5.3.3
+ */
+@RecordApplicationEvents
+class TestNGApplicationEventsIntegrationTests extends AbstractTestNGSpringContextTests {
+
+ @Autowired
+ ApplicationContext context;
+
+ @Autowired
+ ApplicationEvents applicationEvents;
+
+ private boolean testAlreadyExecuted = false;
+
+
+ @BeforeMethod
+ void beforeEach() {
+ if (!testAlreadyExecuted) {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent");
+ }
+ else {
+ assertEventTypes(applicationEvents, "BeforeTestMethodEvent");
+ }
+
+ context.publishEvent(new CustomEvent("beforeEach"));
+ assertThat(applicationEvents.stream(CustomEvent.class)).extracting(CustomEvent::getMessage)//
+ .containsExactly("beforeEach");
+
+ if (!testAlreadyExecuted) {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent");
+ }
+ else {
+ assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent");
+ }
+ }
+
+ @Test
+ void test1() {
+ assertTestExpectations("test1");
+ }
+
+ @Test
+ void test2() {
+ assertTestExpectations("test2");
+ }
+
+ private void assertTestExpectations(String testName) {
+ if (!testAlreadyExecuted) {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
+ "BeforeTestExecutionEvent");
+ }
+ else {
+ assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent");
+ }
+
+ context.publishEvent(new CustomEvent(testName));
+ assertThat(applicationEvents.stream(CustomEvent.class)).extracting(CustomEvent::getMessage)//
+ .containsExactly("beforeEach", testName);
+
+ if (!testAlreadyExecuted) {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
+ "BeforeTestExecutionEvent", "CustomEvent");
+ }
+ else {
+ assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent",
+ "CustomEvent");
+ }
+ }
+
+ @AfterMethod
+ void afterEach(Method testMethod) {
+ if (!testAlreadyExecuted) {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
+ "BeforeTestExecutionEvent", "CustomEvent", "AfterTestExecutionEvent");
+ }
+ else {
+ assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent",
+ "CustomEvent", "AfterTestExecutionEvent");
+ }
+
+ context.publishEvent(new CustomEvent("afterEach"));
+ assertThat(applicationEvents.stream(CustomEvent.class)).extracting(CustomEvent::getMessage)//
+ .containsExactly("beforeEach", testMethod.getName(), "afterEach");
+
+ if (!testAlreadyExecuted) {
+ assertEventTypes(applicationEvents, "PrepareTestInstanceEvent", "BeforeTestMethodEvent", "CustomEvent",
+ "BeforeTestExecutionEvent", "CustomEvent", "AfterTestExecutionEvent", "CustomEvent");
+ testAlreadyExecuted = true;
+ }
+ else {
+ assertEventTypes(applicationEvents, "BeforeTestMethodEvent", "CustomEvent", "BeforeTestExecutionEvent",
+ "CustomEvent", "AfterTestExecutionEvent", "CustomEvent");
+ }
+ }
+
+
+ private static void assertEventTypes(ApplicationEvents applicationEvents, String... types) {
+ assertThat(applicationEvents.stream().map(event -> event.getClass().getSimpleName()))
+ .containsExactly(types);
+ }
+
+
+ @Configuration
+ static class Config {
+ }
+
+ @SuppressWarnings("serial")
+ static class CustomEvent extends ApplicationEvent {
+
+ private final String message;
+
+
+ CustomEvent(String message) {
+ super(message);
+ this.message = message;
+ }
+
+ String getMessage() {
+ return message;
+ }
+ }
+
+}
diff --git a/src/docs/asciidoc/testing.adoc b/src/docs/asciidoc/testing.adoc
index 09b2b3936f..d66590d5a7 100644
--- a/src/docs/asciidoc/testing.adoc
+++ b/src/docs/asciidoc/testing.adoc
@@ -414,6 +414,7 @@ Spring's testing annotations include the following:
* <>
* <>
* <>
+* <>
* <>
* <>
* <>
@@ -1134,6 +1135,19 @@ superclasses or enclosing classes. See
{api-spring-framework}/test/context/TestExecutionListeners.html[`@TestExecutionListeners`
javadoc] for an example and further details.
+[[spring-testing-annotation-recordapplicationevents]]
+===== `@RecordApplicationEvents`
+
+`@RecordApplicationEvents` is a class-level annotation that is used to instruct the
+_Spring TestContext Framework_ to record all application events that are published in the
+`ApplicationContext` during the execution of a single test.
+
+The recorded events can be accessed via the `ApplicationEvents` API within tests.
+
+See <> and the
+{api-spring-framework}/test/context/event/RecordApplicationEvents.html[`@RecordApplicationEvents`
+javadoc] for an example and further details.
+
[[spring-testing-annotation-commit]]
===== `@Commit`
@@ -1880,6 +1894,7 @@ following annotations.
* <>
* <>
* <>
+* <>
* <>
* <>
* <>
@@ -2401,6 +2416,8 @@ by default, exactly in the following order:
`WebApplicationContext`.
* `DirtiesContextBeforeModesTestExecutionListener`: Handles the `@DirtiesContext`
annotation for "`before`" modes.
+* `ApplicationEventsTestExecutionListener`: Provides support for
+ <>.
* `DependencyInjectionTestExecutionListener`: Provides dependency injection for the test
instance.
* `DirtiesContextTestExecutionListener`: Handles the `@DirtiesContext` annotation for
@@ -2547,6 +2564,95 @@ be replaced with the following:
}
----
+[[testcontext-application-events]]
+==== Application Events
+
+Since Spring Framework 5.3.3, the TestContext framework provides support for recording
+<> published in the
+`ApplicationContext` so that assertions can be performed against those events within
+tests. All events published during the execution of a single test are made available via
+the `ApplicationEvents` API which allows you to process the events as a
+`java.util.Stream`.
+
+To use `ApplicationEvents` in your tests, do the following.
+
+* Ensure that your test class is annotated or meta-annotated with
+ <>.
+* Ensure that the `ApplicationEventsTestExecutionListener` is registered. Note, however,
+ that `ApplicationEventsTestExecutionListener` is registered by default and only needs
+ to be manually registered if you have custom configuration via
+ `@TestExecutionListeners` that does not include the default listeners.
+* Annotate a field of type `ApplicationEvents` with `@Autowired` and use that instance of
+ `ApplicationEvents` in your test and lifecycle methods (such as `@BeforeEach` and
+ `@AfterEach` methods in JUnit Jupiter).
+** When using the <>, you may declare a method
+ parameter of type `ApplicationEvents` in a test or lifecycle method as an alternative
+ to an `@Autowired` field in the test class.
+
+The following test class uses the `SpringExtension` for JUnit Jupiter and
+https://assertj.github.io/doc/[AssertJ] to assert the types of application events
+published while invoking a method in a Spring-managed component:
+
+// Don't use "quotes" in the "subs" section because of the asterisks in /* ... */
+[source,java,indent=0,subs="verbatim",role="primary"]
+.Java
+----
+ @SpringJUnitConfig(/* ... */)
+ @RecordApplicationEvents // <1>
+ class OrderServiceTests {
+
+ @Autowired
+ OrderService orderService;
+
+ @Autowired
+ ApplicationEvents events; // <2>
+
+ @Test
+ void submitOrder() {
+ // Invoke method in OrderService that publishes an event
+ orderService.submitOrder(new Order(/* ... */));
+ // Verify that an OrderSubmitted event was published
+ int numEvents = events.stream(OrderSubmitted.class).count(); // <3>
+ assertThat(numEvents).isEqualTo(1);
+ }
+ }
+----
+<1> Annotate the test class with `@RecordApplicationEvents`.
+<2> Inject the `ApplicationEvents` instance for the current test.
+<3> Use the `ApplicationEvents` API to count how many `OrderSubmitted` events were published.
+
+// Don't use "quotes" in the "subs" section because of the asterisks in /* ... */
+[source,kotlin,indent=0,subs="verbatim",role="secondary"]
+.Kotlin
+----
+ @SpringJUnitConfig(/* ... */)
+ @RecordApplicationEvents // <1>
+ class OrderServiceTests {
+
+ @Autowired
+ lateinit var orderService: OrderService
+
+ @Autowired
+ lateinit var events: ApplicationEvents // <2>
+
+ @Test
+ fun submitOrder() {
+ // Invoke method in OrderService that publishes an event
+ orderService.submitOrder(Order(/* ... */))
+ // Verify that an OrderSubmitted event was published
+ val numEvents = events.stream(OrderSubmitted::class).count() // <3>
+ assertThat(numEvents).isEqualTo(1)
+ }
+ }
+----
+<1> Annotate the test class with `@RecordApplicationEvents`.
+<2> Inject the `ApplicationEvents` instance for the current test.
+<3> Use the `ApplicationEvents` API to count how many `OrderSubmitted` events were published.
+
+See the
+{api-spring-framework}/test/context/event/ApplicationEvents.html[`ApplicationEvents`
+javadoc] for further details regarding the `ApplicationEvents` API.
+
[[testcontext-test-execution-events]]
==== Test Execution Events
@@ -7705,7 +7811,7 @@ to create a message, as the following example shows:
----
Finally, we can verify that a new message was created successfully. The following
-assertions use the https://joel-costigliola.github.io/assertj/[AssertJ] library:
+assertions use the https://assertj.github.io/doc/[AssertJ] library:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
@@ -8172,7 +8278,7 @@ annotation to look up our submit button with a `css` selector (*input[type=submi
Finally, we can verify that a new message was created successfully. The following
-assertions use the https://joel-costigliola.github.io/assertj/[AssertJ] assertion library:
+assertions use the https://assertj.github.io/doc/[AssertJ] assertion library:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
@@ -8618,7 +8724,7 @@ See the following resources for more information about testing:
* https://testng.org/[TestNG]: A testing framework inspired by JUnit with added support
for test groups, data-driven testing, distributed testing, and other features. Supported
in the <>
-* https://joel-costigliola.github.io/assertj/[AssertJ]: "`Fluent assertions for Java`",
+* https://assertj.github.io/doc/[AssertJ]: "`Fluent assertions for Java`",
including support for Java 8 lambdas, streams, and other features.
* https://en.wikipedia.org/wiki/Mock_Object[Mock Objects]: Article in Wikipedia.
* http://www.mockobjects.com/[MockObjects.com]: Web site dedicated to mock objects, a