Introduce BEFORE METHOD/CLASS modes in @DirtiesContext

Prior to this commit, @DirtiesContext could only be used to close a
test ApplicationContext after an entire test class or after a test
method; however, there are some use cases for which it would be
beneficial to close a test ApplicationContext before a given test class
or test method -- for example, if some rogue (i.e., yet to be
determined) test within a large test suite has corrupted the original
configuration for the ApplicationContext.

This commit provides a solution to such testing challenges by
introducing the following modes for @DirtiesContext.

 - MethodMode.BEFORE_METHOD: configured via the new methodMode attribute

 - ClassMode.BEFORE_CLASS and ClassMode.BEFORE_EACH_TEST_METHOD: both
   configured via the existing classMode attribute

Issue: SPR-12429
This commit is contained in:
Sam Brannen
2015-03-22 18:42:41 +01:00
parent 6028a64444
commit e086a637d5
4 changed files with 397 additions and 124 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -26,36 +26,49 @@ import java.lang.annotation.Target;
/**
* Test annotation which indicates that the
* {@link org.springframework.context.ApplicationContext ApplicationContext}
* associated with a test is <em>dirty</em> and should be closed:
* associated with a test is <em>dirty</em> and should therefore be closed
* and removed from the context cache.
*
* <ul>
* <li>after the current test, when declared at the method level</li>
* <li>after each test method in the current test class, when declared at the
* class level with class mode set to {@link ClassMode#AFTER_EACH_TEST_METHOD
* AFTER_EACH_TEST_METHOD}</li>
* <li>after the current test class, when declared at the class level with class
* mode set to {@link ClassMode#AFTER_CLASS AFTER_CLASS}</li>
* </ul>
*
* <p>Use this annotation if a test has modified the context &mdash; for example,
* by replacing a bean definition or changing the state of a singleton bean.
* Subsequent tests will be supplied a new context.
* <p>Use this annotation if a test has modified the context &mdash; for
* example, by modifying the state of a singleton bean, modifying the state
* of an embedded database, etc. Subsequent tests that request the same
* context will be supplied a new context.
*
* <p>{@code @DirtiesContext} may be used as a class-level and method-level
* annotation within the same class. In such scenarios, the
* {@code ApplicationContext} will be marked as <em>dirty</em> after any
* such annotated method as well as after the entire class. If the
* {@link ClassMode} is set to {@link ClassMode#AFTER_EACH_TEST_METHOD
* AFTER_EACH_TEST_METHOD}, the context will be marked dirty after each test
* method in the class.
* annotation within the same class or class hierarchy. In such scenarios, the
* {@code ApplicationContext} will be marked as <em>dirty</em> before or
* after any such annotated method as well as before or after the current test
* class, depending on the configured {@link #methodMode} and {@link #classMode}.
*
* <p>As of Spring Framework 4.0, this annotation may be used as a
* <em>meta-annotation</em> to create custom <em>composed annotations</em>.
*
* <h3>Supported Test Phases</h3>
* <ul>
* <li><strong>Before current test class</strong>: when declared at the class
* level with class mode set to {@link ClassMode#BEFORE_CLASS BEFORE_CLASS}</li>
* <li><strong>Before each test method in current test class</strong>: when
* declared at the class level with class mode set to
* {@link ClassMode#BEFORE_EACH_TEST_METHOD BEFORE_EACH_TEST_METHOD}</li>
* <li><strong>Before current test method</strong>: when declared at the
* method level with method mode set to
* {@link MethodMode#BEFORE_METHOD BEFORE_METHOD}</li>
* <li><strong>After current test method</strong>: when declared at the
* method level with method mode set to
* {@link MethodMode#AFTER_METHOD AFTER_METHOD}</li>
* <li><strong>After each test method in current test class</strong>: when
* declared at the class level with class mode set to
* {@link ClassMode#AFTER_EACH_TEST_METHOD AFTER_EACH_TEST_METHOD}</li>
* <li><strong>After current test class</strong>: when declared at the
* class level with class mode set to
* {@link ClassMode#AFTER_CLASS AFTER_CLASS}</li>
* </ul>
*
* @author Sam Brannen
* @author Rod Johnson
* @since 2.0
* @see org.springframework.test.context.ContextConfiguration
* @see org.springframework.test.context.support.DirtiesContextTestExecutionListener
*/
@Documented
@Inherited
@@ -63,6 +76,27 @@ import java.lang.annotation.Target;
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface DirtiesContext {
/**
* Defines <i>modes</i> which determine how {@code @DirtiesContext} is
* interpreted when used to annotate a test method.
*
* @since 4.2
*/
static enum MethodMode {
/**
* The associated {@code ApplicationContext} will be marked as
* <em>dirty</em> before the corresponding test method.
*/
BEFORE_METHOD,
/**
* The associated {@code ApplicationContext} will be marked as
* <em>dirty</em> after the corresponding test method.
*/
AFTER_METHOD;
}
/**
* Defines <i>modes</i> which determine how {@code @DirtiesContext} is
* interpreted when used to annotate a test class.
@@ -73,15 +107,31 @@ public @interface DirtiesContext {
/**
* The associated {@code ApplicationContext} will be marked as
* <em>dirty</em> after the test class.
* <em>dirty</em> before the test class.
*
* @since 4.2
*/
AFTER_CLASS,
BEFORE_CLASS,
/**
* The associated {@code ApplicationContext} will be marked as
* <em>dirty</em> before each test method in the class.
*
* @since 4.2
*/
BEFORE_EACH_TEST_METHOD,
/**
* The associated {@code ApplicationContext} will be marked as
* <em>dirty</em> after each test method in the class.
*/
AFTER_EACH_TEST_METHOD;
AFTER_EACH_TEST_METHOD,
/**
* The associated {@code ApplicationContext} will be marked as
* <em>dirty</em> after the test class.
*/
AFTER_CLASS;
}
/**
@@ -119,13 +169,23 @@ public @interface DirtiesContext {
}
/**
* The <i>mode</i> to use when a test method is annotated with
* {@code @DirtiesContext}.
* <p>Defaults to {@link MethodMode#AFTER_METHOD AFTER_METHOD}.
* <p>Setting the method mode on an annotated test class has no meaning.
* For class-level control, use {@link #classMode} instead.
*
* @since 4.2
*/
MethodMode methodMode() default MethodMode.AFTER_METHOD;
/**
* The <i>mode</i> to use when a test class is annotated with
* {@code @DirtiesContext}.
* <p>Defaults to {@link ClassMode#AFTER_CLASS AFTER_CLASS}.
* <p>Note: Setting the class mode on an annotated test method has no meaning,
* since the mere presence of the {@code @DirtiesContext} annotation on a
* test method is sufficient.
* <p>Setting the class mode on an annotated test method has no meaning.
* For method-level control, use {@link #methodMode} instead.
*
* @since 3.0
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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,16 +27,18 @@ import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.annotation.DirtiesContext.MethodMode;
import org.springframework.test.context.TestContext;
import org.springframework.util.Assert;
import static org.springframework.test.annotation.DirtiesContext.ClassMode.*;
import static org.springframework.test.annotation.DirtiesContext.MethodMode.*;
/**
* {@code TestExecutionListener} which provides support for marking the
* {@code ApplicationContext} associated with a test as <em>dirty</em> for
* both test classes and test methods configured with the {@link DirtiesContext
* &#064;DirtiesContext} annotation.
* both test classes and test methods annotated with the
* {@link DirtiesContext @DirtiesContext} annotation.
*
* @author Sam Brannen
* @author Juergen Hoeller
@@ -57,79 +59,80 @@ public class DirtiesContextTestExecutionListener extends AbstractTestExecutionLi
}
/**
* If the current test method of the supplied {@linkplain TestContext test
* context} is annotated with {@link DirtiesContext &#064;DirtiesContext},
* or if the test class is annotated with {@link DirtiesContext
* &#064;DirtiesContext} and the {@linkplain DirtiesContext#classMode() class
* mode} is set to {@link ClassMode#AFTER_EACH_TEST_METHOD
* AFTER_EACH_TEST_METHOD}, the {@linkplain ApplicationContext application
* context} of the test context will be
* {@linkplain TestContext#markApplicationContextDirty marked as dirty} and the
* {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE}
* in the test context will be set to {@code true}.
* If the test class of the supplied {@linkplain TestContext test context}
* is annotated with {@code @DirtiesContext} and the {@linkplain
* DirtiesContext#classMode() class mode} is set to {@link
* ClassMode#BEFORE_CLASS BEFORE_CLASS}, the {@linkplain ApplicationContext
* application context} of the test context will be
* {@linkplain TestContext#markApplicationContextDirty marked as dirty}, and the
* {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE
* REINJECT_DEPENDENCIES_ATTRIBUTE} in the test context will be set to
* {@code true}.
*/
@Override
public void afterTestMethod(TestContext testContext) throws Exception {
Class<?> testClass = testContext.getTestClass();
Assert.notNull(testClass, "The test class of the supplied TestContext must not be null");
Method testMethod = testContext.getTestMethod();
Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");
final String annotationType = DirtiesContext.class.getName();
AnnotationAttributes methodAnnAttrs = AnnotatedElementUtils.getAnnotationAttributes(testMethod, annotationType);
AnnotationAttributes classAnnAttrs = AnnotatedElementUtils.getAnnotationAttributes(testClass, annotationType);
boolean methodDirtiesContext = methodAnnAttrs != null;
boolean classDirtiesContext = classAnnAttrs != null;
ClassMode classMode = classDirtiesContext ? classAnnAttrs.<ClassMode> getEnum("classMode") : null;
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"After test method: context %s, class dirties context [%s], class mode [%s], method dirties context [%s].",
testContext, classDirtiesContext, classMode, methodDirtiesContext));
}
if (methodDirtiesContext || (classMode == AFTER_EACH_TEST_METHOD)) {
HierarchyMode hierarchyMode = methodDirtiesContext ? methodAnnAttrs.<HierarchyMode> getEnum("hierarchyMode")
: classAnnAttrs.<HierarchyMode> getEnum("hierarchyMode");
dirtyContext(testContext, hierarchyMode);
}
public void beforeTestClass(TestContext testContext) throws Exception {
beforeOrAfterTestClass(testContext, "Before", BEFORE_CLASS);
}
/**
* If the test class of the supplied {@linkplain TestContext test context} is
* annotated with {@link DirtiesContext &#064;DirtiesContext}, the
* {@linkplain ApplicationContext application context} of the test context will
* be {@linkplain TestContext#markApplicationContextDirty marked as dirty},
* and the
* If the current test method of the supplied {@linkplain TestContext test
* context} is annotated with {@code @DirtiesContext} and the {@linkplain
* DirtiesContext#methodMode() method mode} is set to {@link
* MethodMode#BEFORE_METHOD BEFORE_METHOD}, or if the test class is
* annotated with {@code @DirtiesContext} and the {@linkplain
* DirtiesContext#classMode() class mode} is set to {@link
* ClassMode#BEFORE_EACH_TEST_METHOD BEFORE_EACH_TEST_METHOD}, the
* {@linkplain ApplicationContext application context} of the test context
* will be {@linkplain TestContext#markApplicationContextDirty marked as dirty} and the
* {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE
* REINJECT_DEPENDENCIES_ATTRIBUTE} in the test context will be set to {@code true}.
* @since 4.2
*/
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
beforeOrAfterTestMethod(testContext, "Before", BEFORE_METHOD, BEFORE_EACH_TEST_METHOD);
}
/**
* If the current test method of the supplied {@linkplain TestContext test
* context} is annotated with {@code @DirtiesContext} and the {@linkplain
* DirtiesContext#methodMode() method mode} is set to {@link
* MethodMode#AFTER_METHOD AFTER_METHOD}, or if the test class is
* annotated with {@code @DirtiesContext} and the {@linkplain
* DirtiesContext#classMode() class mode} is set to {@link
* ClassMode#AFTER_EACH_TEST_METHOD AFTER_EACH_TEST_METHOD}, the
* {@linkplain ApplicationContext application context} of the test context
* will be {@linkplain TestContext#markApplicationContextDirty marked as dirty} and the
* {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE
* REINJECT_DEPENDENCIES_ATTRIBUTE} in the test context will be set to {@code true}.
*/
@Override
public void afterTestMethod(TestContext testContext) throws Exception {
beforeOrAfterTestMethod(testContext, "After", AFTER_METHOD, AFTER_EACH_TEST_METHOD);
}
/**
* If the test class of the supplied {@linkplain TestContext test context}
* is annotated with {@code @DirtiesContext} and the {@linkplain
* DirtiesContext#classMode() class mode} is set to {@link
* ClassMode#AFTER_CLASS AFTER_CLASS}, the {@linkplain ApplicationContext
* application context} of the test context will be
* {@linkplain TestContext#markApplicationContextDirty marked as dirty}, and the
* {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE
* REINJECT_DEPENDENCIES_ATTRIBUTE} in the test context will be set to
* {@code true}.
*/
@Override
public void afterTestClass(TestContext testContext) throws Exception {
Class<?> testClass = testContext.getTestClass();
Assert.notNull(testClass, "The test class of the supplied TestContext must not be null");
final String annotationType = DirtiesContext.class.getName();
AnnotationAttributes annAttrs = AnnotatedElementUtils.getAnnotationAttributes(testClass, annotationType);
boolean dirtiesContext = annAttrs != null;
if (logger.isDebugEnabled()) {
logger.debug(String.format("After test class: context %s, dirtiesContext [%s].", testContext,
dirtiesContext));
}
if (dirtiesContext) {
HierarchyMode hierarchyMode = annAttrs.<HierarchyMode> getEnum("hierarchyMode");
dirtyContext(testContext, hierarchyMode);
}
beforeOrAfterTestClass(testContext, "After", AFTER_CLASS);
}
/**
* Marks the {@linkplain ApplicationContext application context} of the supplied
* {@linkplain TestContext test context} as
* {@linkplain TestContext#markApplicationContextDirty(DirtiesContext.HierarchyMode) dirty}
* and sets {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE}
* in the test context to {@code true}.
* and sets {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE
* REINJECT_DEPENDENCIES_ATTRIBUTE} in the test context to {@code true}.
* @param testContext the test context whose application context should
* marked as dirty
* @param hierarchyMode the context cache clearing mode to be applied if the
@@ -141,4 +144,62 @@ public class DirtiesContextTestExecutionListener extends AbstractTestExecutionLi
testContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE, Boolean.TRUE);
}
/**
* Perform the actual work for {@link #beforeTestMethod} and {@link #afterTestMethod}.
* @since 4.2
*/
private void beforeOrAfterTestMethod(TestContext testContext, String phase, MethodMode requiredMethodMode,
ClassMode requiredClassMode) throws Exception {
Class<?> testClass = testContext.getTestClass();
Assert.notNull(testClass, "The test class of the supplied TestContext must not be null");
Method testMethod = testContext.getTestMethod();
Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");
final String annotationType = DirtiesContext.class.getName();
AnnotationAttributes methodAnnAttrs = AnnotatedElementUtils.getAnnotationAttributes(testMethod, annotationType);
AnnotationAttributes classAnnAttrs = AnnotatedElementUtils.getAnnotationAttributes(testClass, annotationType);
boolean methodAnnotated = methodAnnAttrs != null;
boolean classAnnotated = classAnnAttrs != null;
MethodMode methodMode = methodAnnotated ? methodAnnAttrs.<MethodMode> getEnum("methodMode") : null;
ClassMode classMode = classAnnotated ? classAnnAttrs.<ClassMode> getEnum("classMode") : null;
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"%s test method: context %s, class annotated with @DirtiesContext [%s] with mode [%s], method annotated with @DirtiesContext [%s] with mode [%s].",
phase, testContext, classAnnotated, classMode, methodAnnotated, methodMode));
}
if ((methodMode == requiredMethodMode) || (classMode == requiredClassMode)) {
HierarchyMode hierarchyMode = methodAnnotated ? methodAnnAttrs.<HierarchyMode> getEnum("hierarchyMode")
: classAnnAttrs.<HierarchyMode> getEnum("hierarchyMode");
dirtyContext(testContext, hierarchyMode);
}
}
/**
* Perform the actual work for {@link #beforeTestClass} and {@link #afterTestClass}.
* @since 4.2
*/
private void beforeOrAfterTestClass(TestContext testContext, String phase, ClassMode requiredClassMode)
throws Exception {
Class<?> testClass = testContext.getTestClass();
Assert.notNull(testClass, "The test class of the supplied TestContext must not be null");
final String annotationType = DirtiesContext.class.getName();
AnnotationAttributes classAnnAttrs = AnnotatedElementUtils.getAnnotationAttributes(testClass, annotationType);
boolean classAnnotated = classAnnAttrs != null;
ClassMode classMode = classAnnotated ? classAnnAttrs.<ClassMode> getEnum("classMode") : null;
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"%s test class: context %s, class annotated with @DirtiesContext [%s] with mode [%s].", phase,
testContext, classAnnotated, classMode));
}
if (classMode == requiredClassMode) {
HierarchyMode hierarchyMode = classAnnAttrs.<HierarchyMode> getEnum("hierarchyMode");
dirtyContext(testContext, hierarchyMode);
}
}
}