diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java index 815f1940d3..a9b188e6e9 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -48,6 +48,8 @@ package org.springframework.test.context; * ServletTestExecutionListener} *
  • {@link org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener * DirtiesContextBeforeModesTestExecutionListener}
  • + *
  • {@link org.springframework.test.context.event.ApplicationEventsTestExecutionListener + * ApplicationEventsTestExecutionListener}
  • *
  • {@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener * DependencyInjectionTestExecutionListener}
  • *
  • {@link org.springframework.test.context.support.DirtiesContextTestExecutionListener diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java index b1b38149d1..444a372f7f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java @@ -67,6 +67,7 @@ public @interface TestExecutionListeners { * {@link #value}, but it may be used instead of {@link #value}. * @see org.springframework.test.context.web.ServletTestExecutionListener * @see org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener + * @see org.springframework.test.context.event.ApplicationEventsTestExecutionListener * @see org.springframework.test.context.support.DependencyInjectionTestExecutionListener * @see org.springframework.test.context.support.DirtiesContextTestExecutionListener * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener diff --git a/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEvents.java b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEvents.java new file mode 100644 index 0000000000..7009fe2376 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEvents.java @@ -0,0 +1,82 @@ +/* + * 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.event; + +import java.util.stream.Stream; + +import org.springframework.context.ApplicationEvent; + +/** + * {@code ApplicationEvents} encapsulates all {@linkplain ApplicationEvent + * application events} that were fired during the execution of a single test method. + * + *

    To use {@code ApplicationEvents} in your tests, do the following. + *

    + * + * @author Sam Brannen + * @author Oliver Drotbohm + * @since 5.3.3 + * @see RecordApplicationEvents + * @see ApplicationEventsTestExecutionListener + * @see org.springframework.context.ApplicationEvent + */ +public interface ApplicationEvents { + + /** + * Stream all application events that were fired during test execution. + * @return a stream of all application events + * @see #stream(Class) + * @see #clear() + */ + Stream stream(); + + /** + * Stream all application events or event payloads of the given type that + * were fired during test execution. + * @param the event type + * @param type the type of events or payloads to stream; never {@code null} + * @return a stream of all application events or event payloads of the + * specified type + * @see #stream() + * @see #clear() + */ + Stream stream(Class type); + + /** + * Clear all application events recorded by this {@code ApplicationEvents} instance. + *

    Subsequent calls to {@link #stream()} or {@link #stream(Class)} will + * only include events recorded since this method was invoked. + * @see #stream() + * @see #stream(Class) + */ + void clear(); + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsApplicationListener.java b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsApplicationListener.java new file mode 100644 index 0000000000..a72d802386 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsApplicationListener.java @@ -0,0 +1,41 @@ +/* + * 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.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; + +/** + * {@link ApplicationListener} that listens to all events and adds them to the + * current {@link ApplicationEvents} instance if registered for the current thread. + * + * @author Sam Brannen + * @author Oliver Drotbohm + * @since 5.3.3 + */ +class ApplicationEventsApplicationListener implements ApplicationListener { + + @Override + public void onApplicationEvent(ApplicationEvent event) { + DefaultApplicationEvents applicationEvents = + (DefaultApplicationEvents) ApplicationEventsHolder.getApplicationEvents(); + if (applicationEvents != null) { + applicationEvents.addEvent(event); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsHolder.java b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsHolder.java new file mode 100644 index 0000000000..aaa8dce302 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsHolder.java @@ -0,0 +1,107 @@ +/* + * 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.event; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Holder class to expose the application events published during the execution + * of a test in the form of a thread-bound {@link ApplicationEvents} object. + * + *

    {@code ApplicationEvents} are registered in this holder and managed by + * the {@link ApplicationEventsTestExecutionListener}. + * + *

    Although this class is {@code public}, it is only intended for use within + * the Spring TestContext Framework or in the implementation of + * third-party extensions. Test authors should therefore allow the current + * instance of {@code ApplicationEvents} to be + * {@link org.springframework.beans.factory.annotation.Autowired @Autowired} + * into a field in the test class or injected via a parameter in test and + * lifecycle methods when using JUnit Jupiter and the {@link + * org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension}. + * + * @author Sam Brannen + * @author Oliver Drotbohm + * @since 5.3.3 + * @see ApplicationEvents + * @see RecordApplicationEvents + * @see ApplicationEventsTestExecutionListener + */ +public abstract class ApplicationEventsHolder { + + private static final ThreadLocal applicationEvents = new ThreadLocal<>(); + + + private ApplicationEventsHolder() { + // no-op to prevent instantiation of this holder class + } + + + /** + * Get the {@link ApplicationEvents} for the current thread. + * @return the current {@code ApplicationEvents}, or {@code null} if not registered + */ + @Nullable + public static ApplicationEvents getApplicationEvents() { + return applicationEvents.get(); + } + + /** + * Get the {@link ApplicationEvents} for the current thread. + * @return the current {@code ApplicationEvents} + * @throws IllegalStateException if an instance of {@code ApplicationEvents} + * has not been registered for the current thread + */ + @Nullable + public static ApplicationEvents getRequiredApplicationEvents() { + ApplicationEvents events = applicationEvents.get(); + Assert.state(events != null, "Failed to retrieve ApplicationEvents for the current thread. " + + "Ensure that your test class is annotated with @RecordApplicationEvents " + + "and that the ApplicationEventsTestExecutionListener is registered."); + return events; + } + + + /** + * Register a new {@link DefaultApplicationEvents} instance to be used for the + * current thread, if necessary. + *

    If {@link #registerApplicationEvents()} has already been called for the + * current thread, this method does not do anything. + */ + static void registerApplicationEventsIfNecessary() { + if (getApplicationEvents() == null) { + registerApplicationEvents(); + } + } + + /** + * Register a new {@link DefaultApplicationEvents} instance to be used for the + * current thread. + */ + static void registerApplicationEvents() { + applicationEvents.set(new DefaultApplicationEvents()); + } + + /** + * Remove the registration of the {@link ApplicationEvents} for the current thread. + */ + static void unregisterApplicationEvents() { + applicationEvents.remove(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsTestExecutionListener.java new file mode 100644 index 0000000000..c2362297e0 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsTestExecutionListener.java @@ -0,0 +1,138 @@ +/* + * 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.event; + +import java.io.Serializable; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.core.Conventions; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestContextAnnotationUtils; +import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.util.Assert; + +/** + * {@code TestExecutionListener} which provides support for {@link ApplicationEvents}. + * + *

    This listener manages the registration of {@code ApplicationEvents} for the + * current thread at various points within the test execution lifecycle and makes + * the current instance of {@code ApplicationEvents} available to tests via an + * {@link org.springframework.beans.factory.annotation.Autowired @Autowired} + * field in the test class. + * + *

    If the test class is not annotated or meta-annotated with + * {@link RecordApplicationEvents @RecordApplicationEvents}, this listener + * effectively does nothing. + * + * @author Sam Brannen + * @since 5.3.3 + * @see ApplicationEvents + * @see ApplicationEventsHolder + */ +public class ApplicationEventsTestExecutionListener extends AbstractTestExecutionListener { + + /** + * Attribute name for a {@link TestContext} attribute which indicates + * whether the test class for the given test context is annotated with + * {@link RecordApplicationEvents @RecordApplicationEvents}. + *

    Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}. + */ + private static final String RECORD_APPLICATION_EVENTS = Conventions.getQualifiedAttributeName( + ApplicationEventsTestExecutionListener.class, "recordApplicationEvents"); + + private static final Object applicationEventsMonitor = new Object(); + + + /** + * Returns {@code 1800}. + */ + @Override + public final int getOrder() { + return 1800; + } + + @Override + public void prepareTestInstance(TestContext testContext) throws Exception { + if (recordApplicationEvents(testContext)) { + registerListenerAndResolvableDependencyIfNecessary(testContext.getApplicationContext()); + ApplicationEventsHolder.registerApplicationEvents(); + } + } + + @Override + public void beforeTestMethod(TestContext testContext) throws Exception { + if (recordApplicationEvents(testContext)) { + // Register a new ApplicationEvents instance for the current thread + // in case the test instance is shared -- for example, in TestNG or + // JUnit Jupiter with @TestInstance(PER_CLASS) semantics. + ApplicationEventsHolder.registerApplicationEventsIfNecessary(); + } + } + + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + if (recordApplicationEvents(testContext)) { + ApplicationEventsHolder.unregisterApplicationEvents(); + } + } + + private boolean recordApplicationEvents(TestContext testContext) { + return testContext.computeAttribute(RECORD_APPLICATION_EVENTS, name -> + TestContextAnnotationUtils.hasAnnotation(testContext.getTestClass(), RecordApplicationEvents.class)); + } + + private void registerListenerAndResolvableDependencyIfNecessary(ApplicationContext applicationContext) { + Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext, + "The ApplicationContext for the test must be an AbstractApplicationContext"); + AbstractApplicationContext aac = (AbstractApplicationContext) applicationContext; + // Synchronize to avoid race condition in parallel test execution + synchronized(applicationEventsMonitor) { + boolean notAlreadyRegistered = aac.getApplicationListeners().stream() + .map(Object::getClass) + .noneMatch(ApplicationEventsApplicationListener.class::equals); + if (notAlreadyRegistered) { + // Register a new ApplicationEventsApplicationListener. + aac.addApplicationListener(new ApplicationEventsApplicationListener()); + + // Register ApplicationEvents as a resolvable dependency for @Autowired support in test classes. + ConfigurableListableBeanFactory beanFactory = aac.getBeanFactory(); + beanFactory.registerResolvableDependency(ApplicationEvents.class, new ApplicationEventsObjectFactory()); + } + } + } + + /** + * Factory that exposes the current {@link ApplicationEvents} object on demand. + */ + @SuppressWarnings("serial") + private static class ApplicationEventsObjectFactory implements ObjectFactory, Serializable { + + @Override + public ApplicationEvents getObject() { + return ApplicationEventsHolder.getRequiredApplicationEvents(); + } + + @Override + public String toString() { + return "Current ApplicationEvents"; + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/event/DefaultApplicationEvents.java b/spring-test/src/main/java/org/springframework/test/context/event/DefaultApplicationEvents.java new file mode 100644 index 0000000000..22d2eff61b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/event/DefaultApplicationEvents.java @@ -0,0 +1,65 @@ +/* + * 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.event; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.PayloadApplicationEvent; + +/** + * Default implementation of {@link ApplicationEvents}. + * + * @author Oliver Drotbohm + * @author Sam Brannen + * @since 5.3.3 + */ +class DefaultApplicationEvents implements ApplicationEvents { + + private final List events = new ArrayList<>(); + + + void addEvent(ApplicationEvent event) { + this.events.add(event); + } + + @Override + public Stream stream() { + return this.events.stream(); + } + + @Override + public Stream stream(Class type) { + return this.events.stream() + .map(this::unwrapPayloadEvent) + .filter(type::isInstance) + .map(type::cast); + } + + @Override + public void clear() { + this.events.clear(); + } + + private Object unwrapPayloadEvent(Object source) { + return (PayloadApplicationEvent.class.isInstance(source) ? + ((PayloadApplicationEvent) source).getPayload() : source); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/event/RecordApplicationEvents.java b/spring-test/src/main/java/org/springframework/test/context/event/RecordApplicationEvents.java new file mode 100644 index 0000000000..4a1e8e0500 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/event/RecordApplicationEvents.java @@ -0,0 +1,49 @@ +/* + * 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.event; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@code @RecordApplicationEvents} is a class-level annotation that is used to + * instruct the Spring TestContext Framework to record all + * {@linkplain org.springframework.context.ApplicationEvent application events} + * that are published in the {@link org.springframework.context.ApplicationContext + * ApplicationContext} during the execution of a single test. + * + *

    The recorded events can be accessed via the {@link ApplicationEvents} API + * within your tests. + * + *

    This annotation may be used as a meta-annotation to create custom + * composed annotations. + * + * @author Sam Brannen + * @since 5.3.3 + * @see ApplicationEvents + * @see ApplicationEventsTestExecutionListener + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface RecordApplicationEvents { +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java index cf72a2b241..83030f37b2 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java @@ -52,6 +52,7 @@ import org.springframework.core.annotation.RepeatableContainers; import org.springframework.lang.Nullable; import org.springframework.test.context.TestConstructor; import org.springframework.test.context.TestContextManager; +import org.springframework.test.context.event.ApplicationEvents; import org.springframework.test.context.support.PropertyProvider; import org.springframework.test.context.support.TestConstructorUtils; import org.springframework.util.Assert; @@ -218,6 +219,7 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes * invoked with a fallback {@link PropertyProvider} that delegates its lookup * to {@link ExtensionContext#getConfigurationParameter(String)}.

  • *
  • The parameter is of type {@link ApplicationContext} or a sub-type thereof.
  • + *
  • The parameter is of type {@link ApplicationEvents} or a sub-type thereof.
  • *
  • {@link ParameterResolutionDelegate#isAutowirable} returns {@code true}.
  • * *

    WARNING: If a test class {@code Constructor} is annotated @@ -238,9 +240,19 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes extensionContext.getConfigurationParameter(propertyName).orElse(null); return (TestConstructorUtils.isAutowirableConstructor(executable, testClass, junitPropertyProvider) || ApplicationContext.class.isAssignableFrom(parameter.getType()) || + supportsApplicationEvents(parameterContext) || ParameterResolutionDelegate.isAutowirable(parameter, parameterContext.getIndex())); } + private boolean supportsApplicationEvents(ParameterContext parameterContext) { + if (ApplicationEvents.class.isAssignableFrom(parameterContext.getParameter().getType())) { + Assert.isTrue(parameterContext.getDeclaringExecutable() instanceof Method, + "ApplicationEvents can only be injected into test and lifecycle methods"); + return true; + } + return false; + } + /** * Resolve a value for the {@link Parameter} in the supplied {@link ParameterContext} by * retrieving the corresponding dependency from the test's {@link ApplicationContext}. diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java index a80ae973ff..164fb5c12d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -27,6 +27,7 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestContext; import org.springframework.test.context.TestContextManager; import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.event.ApplicationEventsTestExecutionListener; import org.springframework.test.context.event.EventPublishingTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener; @@ -54,6 +55,7 @@ import org.springframework.test.context.web.ServletTestExecutionListener; *