diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContext.java b/spring-test/src/main/java/org/springframework/test/context/TestContext.java index 1c1585ff53..59a45a27d9 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContext.java @@ -18,8 +18,10 @@ package org.springframework.test.context; import java.io.Serializable; import java.lang.reflect.Method; +import java.util.function.Function; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; import org.springframework.core.AttributeAccessor; import org.springframework.lang.Nullable; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; @@ -76,6 +78,23 @@ public interface TestContext extends AttributeAccessor, Serializable { */ ApplicationContext getApplicationContext(); + /** + * Publish the {@link ApplicationEvent} created by the given {@code eventFactory} + * to the {@linkplain ApplicationContext application context} for this + * test context. + *

The {@code ApplicationEvent} will only be published if the application + * context for this test context {@linkplain #hasApplicationContext() is available}. + * @param eventFactory factory for lazy creation of the {@code ApplicationEvent} + * @since 5.2 + * @see #hasApplicationContext() + * @see #getApplicationContext() + */ + default void publishEvent(Function eventFactory) { + if (hasApplicationContext()) { + getApplicationContext().publishEvent(eventFactory.apply(this)); + } + } + /** * Get the {@linkplain Class test class} for this test context. * @return the test class (never {@code null}) diff --git a/spring-test/src/main/java/org/springframework/test/context/event/EventPublishingTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/event/EventPublishingTestExecutionListener.java index 1ac348f0f9..d25af05b3d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/event/EventPublishingTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/event/EventPublishingTestExecutionListener.java @@ -16,8 +16,6 @@ package org.springframework.test.context.event; -import java.util.function.Function; - import org.springframework.test.context.TestContext; import org.springframework.test.context.TestExecutionListener; import org.springframework.test.context.support.AbstractTestExecutionListener; @@ -92,7 +90,7 @@ public class EventPublishingTestExecutionListener extends AbstractTestExecutionL */ @Override public void beforeTestClass(TestContext testContext) { - publishEvent(testContext, BeforeTestClassEvent::new); + testContext.publishEvent(BeforeTestClassEvent::new); } /** @@ -101,7 +99,7 @@ public class EventPublishingTestExecutionListener extends AbstractTestExecutionL */ @Override public void prepareTestInstance(TestContext testContext) { - publishEvent(testContext, PrepareTestInstanceEvent::new); + testContext.publishEvent(PrepareTestInstanceEvent::new); } /** @@ -110,7 +108,7 @@ public class EventPublishingTestExecutionListener extends AbstractTestExecutionL */ @Override public void beforeTestMethod(TestContext testContext) { - publishEvent(testContext, BeforeTestMethodEvent::new); + testContext.publishEvent(BeforeTestMethodEvent::new); } /** @@ -119,7 +117,7 @@ public class EventPublishingTestExecutionListener extends AbstractTestExecutionL */ @Override public void beforeTestExecution(TestContext testContext) { - publishEvent(testContext, BeforeTestExecutionEvent::new); + testContext.publishEvent(BeforeTestExecutionEvent::new); } /** @@ -128,7 +126,7 @@ public class EventPublishingTestExecutionListener extends AbstractTestExecutionL */ @Override public void afterTestExecution(TestContext testContext) { - publishEvent(testContext, AfterTestExecutionEvent::new); + testContext.publishEvent(AfterTestExecutionEvent::new); } /** @@ -137,7 +135,7 @@ public class EventPublishingTestExecutionListener extends AbstractTestExecutionL */ @Override public void afterTestMethod(TestContext testContext) { - publishEvent(testContext, AfterTestMethodEvent::new); + testContext.publishEvent(AfterTestMethodEvent::new); } /** @@ -146,13 +144,7 @@ public class EventPublishingTestExecutionListener extends AbstractTestExecutionL */ @Override public void afterTestClass(TestContext testContext) { - publishEvent(testContext, AfterTestClassEvent::new); - } - - private void publishEvent(TestContext testContext, Function eventFactory) { - if (testContext.hasApplicationContext()) { - testContext.getApplicationContext().publishEvent(eventFactory.apply(testContext)); - } + testContext.publishEvent(AfterTestClassEvent::new); } } diff --git a/spring-test/src/test/java/org/springframework/test/context/event/CustomTestEventTests.java b/spring-test/src/test/java/org/springframework/test/context/event/CustomTestEventTests.java new file mode 100644 index 0000000000..14c3a11032 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/event/CustomTestEventTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2019 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.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.event.CustomTestEventTests.CustomEventPublishingTestExecutionListener; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS; + +/** + * Integration tests for custom event publication via + * {@link TestContext#publishEvent(java.util.function.Function)}. + * + * @author Sam Brannen + * @since 5.2 + */ +@RunWith(SpringRunner.class) +@TestExecutionListeners(listeners = CustomEventPublishingTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS) +public class CustomTestEventTests { + + private static final List events = new ArrayList<>(); + + + @Before + public void clearEvents() { + events.clear(); + } + + @Test + public void customTestEventPublished() { + assertThat(events).size().isEqualTo(1); + CustomEvent customEvent = events.get(0); + assertThat(customEvent.getSource()).isEqualTo(getClass()); + assertThat(customEvent.getTestName()).isEqualTo("customTestEventPublished"); + } + + + @Configuration + static class Config { + + @EventListener + void processCustomEvent(CustomEvent event) { + events.add(event); + } + } + + @SuppressWarnings("serial") + static class CustomEvent extends ApplicationEvent { + + private final Method testMethod; + + + public CustomEvent(Class testClass, Method testMethod) { + super(testClass); + this.testMethod = testMethod; + } + + String getTestName() { + return this.testMethod.getName(); + } + } + + static class CustomEventPublishingTestExecutionListener implements TestExecutionListener { + + @Override + public void beforeTestExecution(TestContext testContext) throws Exception { + testContext.publishEvent(tc -> new CustomEvent(tc.getTestClass(), tc.getTestMethod())); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/event/EventPublishingTestExecutionListenerIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/event/EventPublishingTestExecutionListenerIntegrationTests.java index 687e99be51..cd7bc64687 100644 --- a/spring-test/src/test/java/org/springframework/test/context/event/EventPublishingTestExecutionListenerIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/event/EventPublishingTestExecutionListenerIntegrationTests.java @@ -77,9 +77,13 @@ public class EventPublishingTestExecutionListenerIntegrationTests { private final TestContextManager testContextManager = new TestContextManager(ExampleTestCase.class); private final TestContext testContext = testContextManager.getTestContext(); + // Note that the following invocation of getApplicationContext() forces eager + // loading of the test's ApplicationContext which consequently results in the + // publication of all test execution events. Otherwise, TestContext#publishEvent + // would never fire any events for ExampleTestCase. private final TestExecutionListener listener = testContext.getApplicationContext().getBean(TestExecutionListener.class); private final Object testInstance = new ExampleTestCase(); - private final Method testMethod = ReflectionUtils.findMethod(ExampleTestCase.class, "traceableTest"); + private final Method traceableTestMethod = ReflectionUtils.findMethod(ExampleTestCase.class, "traceableTest"); @Rule public final ExpectedException exception = ExpectedException.none(); @@ -104,7 +108,7 @@ public class EventPublishingTestExecutionListenerIntegrationTests { @Test public void beforeTestMethodAnnotation() throws Exception { - testContextManager.beforeTestMethod(testInstance, testMethod); + testContextManager.beforeTestMethod(testInstance, traceableTestMethod); verify(listener, only()).beforeTestMethod(testContext); } @@ -162,19 +166,19 @@ public class EventPublishingTestExecutionListenerIntegrationTests { @Test public void beforeTestExecutionAnnotation() throws Exception { - testContextManager.beforeTestExecution(testInstance, testMethod); + testContextManager.beforeTestExecution(testInstance, traceableTestMethod); verify(listener, only()).beforeTestExecution(testContext); } @Test public void afterTestExecutionAnnotation() throws Exception { - testContextManager.afterTestExecution(testInstance, testMethod, null); + testContextManager.afterTestExecution(testInstance, traceableTestMethod, null); verify(listener, only()).afterTestExecution(testContext); } @Test public void afterTestMethodAnnotation() throws Exception { - testContextManager.afterTestMethod(testInstance, testMethod, null); + testContextManager.afterTestMethod(testInstance, traceableTestMethod, null); verify(listener, only()).afterTestMethod(testContext); } diff --git a/spring-test/src/test/java/org/springframework/test/context/event/EventPublishingTestExecutionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/event/EventPublishingTestExecutionListenerTests.java index 2ab00c4dce..3f57d5e26c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/event/EventPublishingTestExecutionListenerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/event/EventPublishingTestExecutionListenerTests.java @@ -17,28 +17,27 @@ package org.springframework.test.context.event; import java.util.function.Consumer; +import java.util.function.Function; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; -import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; import org.springframework.test.context.TestContext; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.only; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; /** * Unit tests for {@link EventPublishingTestExecutionListener}. @@ -52,21 +51,26 @@ public class EventPublishingTestExecutionListenerTests { private final EventPublishingTestExecutionListener listener = new EventPublishingTestExecutionListener(); - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private TestContext testContext; - - @Captor - private ArgumentCaptor testExecutionEvent; - @Rule public final TestName testName = new TestName(); + @Mock + private TestContext testContext; + + @Mock + private ApplicationContext applicationContext; + + @Captor + private ArgumentCaptor> eventFactory; + @Before public void configureMock() { - if (testName.getMethodName().startsWith("publish")) { - when(testContext.hasApplicationContext()).thenReturn(true); - } + // Force Mockito to invoke the interface default method + doCallRealMethod().when(testContext).publishEvent(any()); + when(testContext.getApplicationContext()).thenReturn(applicationContext); + // Only allow events to be published for test methods named "publish*". + when(testContext.hasApplicationContext()).thenReturn(testName.getMethodName().startsWith("publish")); } @Test @@ -104,51 +108,71 @@ public class EventPublishingTestExecutionListenerTests { assertEvent(AfterTestClassEvent.class, listener::afterTestClass); } - private void assertEvent(Class eventClass, Consumer callback) { - callback.accept(testContext); - verify(testContext.getApplicationContext(), only()).publishEvent(testExecutionEvent.capture()); - assertThat(testExecutionEvent.getValue(), instanceOf(eventClass)); - assertThat(testExecutionEvent.getValue().getSource(), equalTo(testContext)); - } - @Test public void doesNotPublishBeforeTestClassEventIfApplicationContextHasNotBeenLoaded() { - assertNoEvent(listener::beforeTestClass); + assertNoEvent(BeforeTestClassEvent.class, listener::beforeTestClass); } @Test public void doesNotPublishPrepareTestInstanceEventIfApplicationContextHasNotBeenLoaded() { - assertNoEvent(listener::prepareTestInstance); + assertNoEvent(PrepareTestInstanceEvent.class, listener::prepareTestInstance); } @Test public void doesNotPublishBeforeTestMethodEventIfApplicationContextHasNotBeenLoaded() { - assertNoEvent(listener::beforeTestMethod); + assertNoEvent(BeforeTestMethodEvent.class, listener::beforeTestMethod); } @Test public void doesNotPublishBeforeTestExecutionEventIfApplicationContextHasNotBeenLoaded() { - assertNoEvent(listener::beforeTestExecution); + assertNoEvent(BeforeTestExecutionEvent.class, listener::beforeTestExecution); } @Test public void doesNotPublishAfterTestExecutionEventIfApplicationContextHasNotBeenLoaded() { - assertNoEvent(listener::afterTestExecution); + assertNoEvent(AfterTestExecutionEvent.class, listener::afterTestExecution); } @Test public void doesNotPublishAfterTestMethodEventIfApplicationContextHasNotBeenLoaded() { - assertNoEvent(listener::afterTestMethod); + assertNoEvent(AfterTestMethodEvent.class, listener::afterTestMethod); } @Test public void doesNotPublishAfterTestClassEventIfApplicationContextHasNotBeenLoaded() { - assertNoEvent(listener::afterTestClass); + assertNoEvent(AfterTestClassEvent.class, listener::afterTestClass); } - private void assertNoEvent(Consumer callback) { + private void assertEvent(Class eventClass, Consumer callback) { callback.accept(testContext); - verify(testContext.getApplicationContext(), never()).publishEvent(any()); + + // The listener attempted to publish the event... + verify(testContext, times(1)).publishEvent(eventFactory.capture()); + + // The listener successfully published the event... + verify(applicationContext, times(1)).publishEvent(any()); + + // Verify the type of event that was published. + ApplicationEvent event = eventFactory.getValue().apply(testContext); + assertThat(event, instanceOf(eventClass)); + assertThat(event.getSource(), equalTo(testContext)); + } + + private void assertNoEvent(Class eventClass, Consumer callback) { + callback.accept(testContext); + + // The listener attempted to publish the event... + verify(testContext, times(1)).publishEvent(eventFactory.capture()); + + // But the event was not actually published since the ApplicationContext + // was not available. + verify(applicationContext, never()).publishEvent(any()); + + // In any case, we can still verify the type of event that would have + // been published. + ApplicationEvent event = eventFactory.getValue().apply(testContext); + assertThat(event, instanceOf(eventClass)); + assertThat(event.getSource(), equalTo(testContext)); } }