Support conditional processing in TestContext event annotations

This commit introduces support for SpEL expressions for conditional
event processing in annotations such as @BeforeTestClass,
@BeforeTestMethod, etc.

This commit also introduces a new getTestContext() method in
TestContextEvent in order to improve the readability of such SpEL
expressions.

See gh-18490
This commit is contained in:
Sam Brannen 2019-03-02 17:26:57 +01:00
parent f6d62eda8b
commit e272e2e151
9 changed files with 147 additions and 26 deletions

View File

@ -39,12 +39,25 @@ public abstract class TestContextEvent extends ApplicationEvent {
} }
/** /**
* Get the {@code TestContext} associated with this event. * Get the {@link TestContext} associated with this event.
* @return the {@code TestContext} associated with this event (never {@code null}) * @return the {@code TestContext} associated with this event (never {@code null})
* @see #getTestContext()
*/ */
@Override @Override
public TestContext getSource() { public final TestContext getSource() {
return (TestContext) super.getSource(); return (TestContext) super.getSource();
} }
/**
* Alias for {@link #getSource()}.
* <p>This method may be favored over {@code getSource()} to improve readability
* in SpEL expressions for event processing
* {@linkplain org.springframework.context.event.EventListener#condition conditions}.
* @return the {@code TestContext} associated with this event (never {@code null})
* @see #getSource()
*/
public final TestContext getTestContext() {
return getSource();
}
} }

View File

@ -21,6 +21,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.event.AfterTestClassEvent; import org.springframework.test.context.event.AfterTestClassEvent;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
@ -41,17 +42,29 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* {@link org.springframework.test.context.TestExecutionListener#afterTestClass} * {@link org.springframework.test.context.TestExecutionListener#afterTestClass}
* lifecycle. * lifecycle.
* *
* <p>Event processing can optionally be made {@linkplain #value conditional} via
* a SpEL expression &mdash; for example,
* {@code @AfterTestClass("event.testContext.testClass.name matches '.+IntegrationTests'")}.
*
* <p>The {@code EventPublishingTestExecutionListener} must be registered in order * <p>The {@code EventPublishingTestExecutionListener} must be registered in order
* for this annotation to have an effect &mdash; for example, via * for this annotation to have an effect &mdash; for example, via
* {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}. * {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}.
* *
* @author Frank Scheffler * @author Frank Scheffler
* @author Sam Brannen
* @since 5.2 * @since 5.2
* @see AfterTestClassEvent * @see AfterTestClassEvent
*/ */
@Documented
@Retention(RUNTIME) @Retention(RUNTIME)
@Target({ METHOD, ANNOTATION_TYPE }) @Target({ METHOD, ANNOTATION_TYPE })
@Documented
@EventListener(AfterTestClassEvent.class) @EventListener(AfterTestClassEvent.class)
public @interface AfterTestClass { public @interface AfterTestClass {
/**
* Alias for {@link EventListener#condition}.
*/
@AliasFor(annotation = EventListener.class, attribute = "condition")
String value() default "";
} }

View File

@ -21,6 +21,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.event.AfterTestExecutionEvent; import org.springframework.test.context.event.AfterTestExecutionEvent;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
@ -41,17 +42,29 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* {@link org.springframework.test.context.TestExecutionListener#afterTestExecution} * {@link org.springframework.test.context.TestExecutionListener#afterTestExecution}
* lifecycle. * lifecycle.
* *
* <p>Event processing can optionally be made {@linkplain #value conditional} via
* a SpEL expression &mdash; for example,
* {@code @AfterTestExecution("event.testContext.testMethod.name matches 'test.*'")}.
*
* <p>The {@code EventPublishingTestExecutionListener} must be registered in order * <p>The {@code EventPublishingTestExecutionListener} must be registered in order
* for this annotation to have an effect &mdash; for example, via * for this annotation to have an effect &mdash; for example, via
* {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}. * {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}.
* *
* @author Frank Scheffler * @author Frank Scheffler
* @author Sam Brannen
* @since 5.2 * @since 5.2
* @see AfterTestExecutionEvent * @see AfterTestExecutionEvent
*/ */
@Documented
@Retention(RUNTIME) @Retention(RUNTIME)
@Target({ METHOD, ANNOTATION_TYPE }) @Target({ METHOD, ANNOTATION_TYPE })
@Documented
@EventListener(AfterTestExecutionEvent.class) @EventListener(AfterTestExecutionEvent.class)
public @interface AfterTestExecution { public @interface AfterTestExecution {
/**
* Alias for {@link EventListener#condition}.
*/
@AliasFor(annotation = EventListener.class, attribute = "condition")
String value() default "";
} }

View File

@ -21,6 +21,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.event.AfterTestMethodEvent; import org.springframework.test.context.event.AfterTestMethodEvent;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
@ -41,17 +42,29 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* {@link org.springframework.test.context.TestExecutionListener#afterTestMethod} * {@link org.springframework.test.context.TestExecutionListener#afterTestMethod}
* lifecycle. * lifecycle.
* *
* <p>Event processing can optionally be made {@linkplain #value conditional} via
* a SpEL expression &mdash; for example,
* {@code @AfterTestMethod("event.testContext.testMethod.name matches 'test.*'")}.
*
* <p>The {@code EventPublishingTestExecutionListener} must be registered in order * <p>The {@code EventPublishingTestExecutionListener} must be registered in order
* for this annotation to have an effect &mdash; for example, via * for this annotation to have an effect &mdash; for example, via
* {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}. * {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}.
* *
* @author Frank Scheffler * @author Frank Scheffler
* @author Sam Brannen
* @since 5.2 * @since 5.2
* @see AfterTestMethodEvent * @see AfterTestMethodEvent
*/ */
@Documented
@Retention(RUNTIME) @Retention(RUNTIME)
@Target({ METHOD, ANNOTATION_TYPE }) @Target({ METHOD, ANNOTATION_TYPE })
@Documented
@EventListener(AfterTestMethodEvent.class) @EventListener(AfterTestMethodEvent.class)
public @interface AfterTestMethod { public @interface AfterTestMethod {
/**
* Alias for {@link EventListener#condition}.
*/
@AliasFor(annotation = EventListener.class, attribute = "condition")
String value() default "";
} }

View File

@ -21,6 +21,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.event.BeforeTestClassEvent; import org.springframework.test.context.event.BeforeTestClassEvent;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
@ -41,17 +42,29 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* {@link org.springframework.test.context.TestExecutionListener#beforeTestClass} * {@link org.springframework.test.context.TestExecutionListener#beforeTestClass}
* lifecycle. * lifecycle.
* *
* <p>Event processing can optionally be made {@linkplain #value conditional} via
* a SpEL expression &mdash; for example,
* {@code @BeforeTestClass("event.testContext.testClass.name matches '.+IntegrationTests'")}.
*
* <p>The {@code EventPublishingTestExecutionListener} must be registered in order * <p>The {@code EventPublishingTestExecutionListener} must be registered in order
* for this annotation to have an effect &mdash; for example, via * for this annotation to have an effect &mdash; for example, via
* {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}. * {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}.
* *
* @author Frank Scheffler * @author Frank Scheffler
* @author Sam Brannen
* @since 5.2 * @since 5.2
* @see BeforeTestClassEvent * @see BeforeTestClassEvent
*/ */
@Documented
@Retention(RUNTIME) @Retention(RUNTIME)
@Target({ METHOD, ANNOTATION_TYPE }) @Target({ METHOD, ANNOTATION_TYPE })
@Documented
@EventListener(BeforeTestClassEvent.class) @EventListener(BeforeTestClassEvent.class)
public @interface BeforeTestClass { public @interface BeforeTestClass {
/**
* Alias for {@link EventListener#condition}.
*/
@AliasFor(annotation = EventListener.class, attribute = "condition")
String value() default "";
} }

View File

@ -21,6 +21,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.event.BeforeTestExecutionEvent; import org.springframework.test.context.event.BeforeTestExecutionEvent;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
@ -41,17 +42,29 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* {@link org.springframework.test.context.TestExecutionListener#beforeTestExecution} * {@link org.springframework.test.context.TestExecutionListener#beforeTestExecution}
* lifecycle. * lifecycle.
* *
* <p>Event processing can optionally be made {@linkplain #value conditional} via
* a SpEL expression &mdash; for example,
* {@code @BeforeTestExecution("event.testContext.testMethod.name matches 'test.*'")}.
*
* <p>The {@code EventPublishingTestExecutionListener} must be registered in order * <p>The {@code EventPublishingTestExecutionListener} must be registered in order
* for this annotation to have an effect &mdash; for example, via * for this annotation to have an effect &mdash; for example, via
* {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}. * {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}.
* *
* @author Frank Scheffler * @author Frank Scheffler
* @author Sam Brannen
* @since 5.2 * @since 5.2
* @see BeforeTestExecutionEvent * @see BeforeTestExecutionEvent
*/ */
@Documented
@Retention(RUNTIME) @Retention(RUNTIME)
@Target({ METHOD, ANNOTATION_TYPE }) @Target({ METHOD, ANNOTATION_TYPE })
@Documented
@EventListener(BeforeTestExecutionEvent.class) @EventListener(BeforeTestExecutionEvent.class)
public @interface BeforeTestExecution { public @interface BeforeTestExecution {
/**
* Alias for {@link EventListener#condition}.
*/
@AliasFor(annotation = EventListener.class, attribute = "condition")
String value() default "";
} }

View File

@ -21,6 +21,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.event.BeforeTestMethodEvent; import org.springframework.test.context.event.BeforeTestMethodEvent;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
@ -41,17 +42,29 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* {@link org.springframework.test.context.TestExecutionListener#beforeTestMethod} * {@link org.springframework.test.context.TestExecutionListener#beforeTestMethod}
* lifecycle. * lifecycle.
* *
* <p>Event processing can optionally be made {@linkplain #value conditional} via
* a SpEL expression &mdash; for example,
* {@code @BeforeTestMethod("event.testContext.testMethod.name matches 'test.*'")}.
*
* <p>The {@code EventPublishingTestExecutionListener} must be registered in order * <p>The {@code EventPublishingTestExecutionListener} must be registered in order
* for this annotation to have an effect &mdash; for example, via * for this annotation to have an effect &mdash; for example, via
* {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}. * {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}.
* *
* @author Frank Scheffler * @author Frank Scheffler
* @author Sam Brannen
* @since 5.2 * @since 5.2
* @see BeforeTestMethodEvent * @see BeforeTestMethodEvent
*/ */
@Documented
@Retention(RUNTIME) @Retention(RUNTIME)
@Target({ METHOD, ANNOTATION_TYPE }) @Target({ METHOD, ANNOTATION_TYPE })
@Documented
@EventListener(BeforeTestMethodEvent.class) @EventListener(BeforeTestMethodEvent.class)
public @interface BeforeTestMethod { public @interface BeforeTestMethod {
/**
* Alias for {@link EventListener#condition}.
*/
@AliasFor(annotation = EventListener.class, attribute = "condition")
String value() default "";
} }

View File

@ -21,6 +21,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.event.PrepareTestInstanceEvent; import org.springframework.test.context.event.PrepareTestInstanceEvent;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
@ -41,17 +42,29 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* {@link org.springframework.test.context.TestExecutionListener#prepareTestInstance} * {@link org.springframework.test.context.TestExecutionListener#prepareTestInstance}
* lifecycle. * lifecycle.
* *
* <p>Event processing can optionally be made {@linkplain #value conditional} via
* a SpEL expression &mdash; for example,
* {@code @PrepareTestInstance("event.testContext.testClass.name matches '.+IntegrationTests'")}.
*
* <p>The {@code EventPublishingTestExecutionListener} must be registered in order * <p>The {@code EventPublishingTestExecutionListener} must be registered in order
* for this annotation to have an effect &mdash; for example, via * for this annotation to have an effect &mdash; for example, via
* {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}. * {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}.
* *
* @author Frank Scheffler * @author Frank Scheffler
* @author Sam Brannen
* @since 5.2 * @since 5.2
* @see PrepareTestInstanceEvent * @see PrepareTestInstanceEvent
*/ */
@Documented
@Retention(RUNTIME) @Retention(RUNTIME)
@Target({ METHOD, ANNOTATION_TYPE }) @Target({ METHOD, ANNOTATION_TYPE })
@Documented
@EventListener(PrepareTestInstanceEvent.class) @EventListener(PrepareTestInstanceEvent.class)
public @interface PrepareTestInstance { public @interface PrepareTestInstance {
/**
* Alias for {@link EventListener#condition}.
*/
@AliasFor(annotation = EventListener.class, attribute = "condition")
String value() default "";
} }

View File

@ -16,6 +16,7 @@
package org.springframework.test.context.event; package org.springframework.test.context.event;
import java.lang.annotation.Retention;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import org.junit.Before; import org.junit.Before;
@ -35,8 +36,11 @@ import org.springframework.test.context.event.annotation.BeforeTestClass;
import org.springframework.test.context.event.annotation.BeforeTestExecution; import org.springframework.test.context.event.annotation.BeforeTestExecution;
import org.springframework.test.context.event.annotation.BeforeTestMethod; import org.springframework.test.context.event.annotation.BeforeTestMethod;
import org.springframework.test.context.event.annotation.PrepareTestInstance; import org.springframework.test.context.event.annotation.PrepareTestInstance;
import org.springframework.util.ReflectionUtils;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.only; import static org.mockito.Mockito.only;
import static org.mockito.Mockito.reset; import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -51,11 +55,11 @@ import static org.mockito.Mockito.verify;
*/ */
public class EventPublishingTestExecutionListenerIntegrationTests { public class EventPublishingTestExecutionListenerIntegrationTests {
private final TestContextManager testContextManager = new TestContextManager(TestCase.class); private final TestContextManager testContextManager = new TestContextManager(ExampleTestCase.class);
private final TestContext testContext = testContextManager.getTestContext(); private final TestContext testContext = testContextManager.getTestContext();
private final TestExecutionListener listener = testContext.getApplicationContext().getBean(EventCaptureConfiguration.class).listener(); private final TestExecutionListener listener = testContext.getApplicationContext().getBean(EventCaptureConfiguration.class).listener();
private final Object testInstance = new TestCase(); private final Object testInstance = new ExampleTestCase();
private final Method testMethod = null; private final Method testMethod = ReflectionUtils.findMethod(ExampleTestCase.class, "test1");
@Before @Before
@ -78,11 +82,18 @@ public class EventPublishingTestExecutionListenerIntegrationTests {
} }
@Test @Test
public void beforeTestMethodAnnotation() throws Exception { public void beforeTestMethodAnnotationWithMatchingCondition() throws Exception {
testContextManager.beforeTestMethod(testInstance, testMethod); testContextManager.beforeTestMethod(testInstance, testMethod);
verify(listener, only()).beforeTestMethod(testContext); verify(listener, only()).beforeTestMethod(testContext);
} }
@Test
public void beforeTestMethodAnnotationWithFailingCondition() throws Exception {
Method testMethod2 = ReflectionUtils.findMethod(ExampleTestCase.class, "test2");
testContextManager.beforeTestMethod(testInstance, testMethod2);
verify(listener, never()).beforeTestMethod(testContext);
}
@Test @Test
public void beforeTestExecutionAnnotation() throws Exception { public void beforeTestExecutionAnnotation() throws Exception {
testContextManager.beforeTestExecution(testInstance, testMethod); testContextManager.beforeTestExecution(testInstance, testMethod);
@ -116,17 +127,17 @@ public class EventPublishingTestExecutionListenerIntegrationTests {
return mock(TestExecutionListener.class); return mock(TestExecutionListener.class);
} }
@BeforeTestClass @BeforeTestClass("#root.event.source.testClass.name matches '.+TestCase'")
public void beforeTestClass(BeforeTestClassEvent e) throws Exception { public void beforeTestClass(BeforeTestClassEvent e) throws Exception {
listener().beforeTestClass(e.getSource()); listener().beforeTestClass(e.getSource());
} }
@PrepareTestInstance @PrepareTestInstance("#a0.testContext.testClass.name matches '.+TestCase'")
public void prepareTestInstance(PrepareTestInstanceEvent e) throws Exception { public void prepareTestInstance(PrepareTestInstanceEvent e) throws Exception {
listener().prepareTestInstance(e.getSource()); listener().prepareTestInstance(e.getSource());
} }
@BeforeTestMethod @BeforeTestMethod("#p0.testContext.testMethod.isAnnotationPresent(T(org.springframework.test.context.event.EventPublishingTestExecutionListenerIntegrationTests.Traceable))")
public void beforeTestMethod(BeforeTestMethodEvent e) throws Exception { public void beforeTestMethod(BeforeTestMethodEvent e) throws Exception {
listener().beforeTestMethod(e.getSource()); listener().beforeTestMethod(e.getSource());
} }
@ -141,27 +152,33 @@ public class EventPublishingTestExecutionListenerIntegrationTests {
listener().afterTestExecution(e.getSource()); listener().afterTestExecution(e.getSource());
} }
@AfterTestMethod @AfterTestMethod("event.testContext.testMethod.isAnnotationPresent(T(org.springframework.test.context.event.EventPublishingTestExecutionListenerIntegrationTests.Traceable))")
public void afterTestMethod(AfterTestMethodEvent e) throws Exception { public void afterTestMethod(AfterTestMethodEvent e) throws Exception {
listener().afterTestMethod(e.getSource()); listener().afterTestMethod(e.getSource());
} }
@AfterTestClass @AfterTestClass("#afterTestClassEvent.testContext.testClass.name matches '.+TestCase'")
public void afterTestClass(AfterTestClassEvent e) throws Exception { public void afterTestClass(AfterTestClassEvent afterTestClassEvent) throws Exception {
listener().afterTestClass(e.getSource()); listener().afterTestClass(afterTestClassEvent.getSource());
} }
} }
@Retention(RUNTIME)
@interface Traceable {
}
@ContextConfiguration(classes = EventCaptureConfiguration.class) @ContextConfiguration(classes = EventCaptureConfiguration.class)
@TestExecutionListeners(EventPublishingTestExecutionListener.class) @TestExecutionListeners(EventPublishingTestExecutionListener.class)
static class TestCase { static class ExampleTestCase {
/** @Traceable
* Serves as dummy test method. public void test1() {
*/ /* no-op */
@SuppressWarnings("PMD.UncommentedEmptyMethodBody") }
public void dummyTestMethod() {
public void test2() {
/* no-op */
} }
} }