Support meta-annotation attr overrides in the TCF

Prior to this commit, the Spring TestContext Framework (TCF) supported
the use of test-related annotations as meta-annotations for composing
custom test stereotype annotations; however, attributes in custom
stereotypes could not be used to override meta-annotation attributes.

This commit addresses this by allowing attributes from the following
annotations (when used as meta-annotations) to be overridden in custom
stereotypes.

- @ContextConfiguration
- @ActiveProfiles
- @DirtiesContext
- @TransactionConfiguration
- @Timed
- @TestExecutionListeners

This support depends on functionality provided by
AnnotatedElementUtils. See the 'Notes' below for further details and
ramifications.

Notes:

- AnnotatedElementUtils does not support overrides for the 'value'
  attribute of an annotation. It is therefore not possible or not
  feasible to support meta-annotation attribute overrides for some
  test-related annotations.
- @ContextHierarchy, @WebAppConfiguration, @Rollback, @Repeat, and
  @ProfileValueSourceConfiguration define single 'value' attributes
  which cannot be overridden via Spring's meta-annotation attribute
  support.
- Although @IfProfileValue has 'values' and 'name' attributes, the
  typical usage scenario involves the 'value' attribute which is not
  supported for meta-annotation attribute overrides. Furthermore,
  'name' and 'values' are so generic that it is deemed unfeasible to
  provide meta-annotation attribute override support for these.
- @BeforeTransaction and @AfterTransaction do not define any attributes
  that can be overridden.
- Support for meta-annotation attribute overrides for @Transactional is
  provided indirectly via SpringTransactionAnnotationParser.

Implementation Details:

- MetaAnnotationUtils.AnnotationDescriptor now provides access to the
  AnnotationAttributes for the described annotation.
- MetaAnnotationUtils.AnnotationDescriptor now provides access to the
  root declaring class as well as the declaring class.
- ContextLoaderUtils now retrieves AnnotationAttributes from
  AnnotationDescriptor to look up annotation attributes for
  @ContextConfiguration and @ActiveProfiles.
- ContextConfigurationAttributes now provides a constructor to have its
  attributes sourced from an instance of AnnotationAttributes.
- ContextLoaderUtils.resolveContextHierarchyAttributes() now throws an
  IllegalStateException if no class in the class hierarchy declares
  @ContextHierarchy.
- TransactionalTestExecutionListener now uses AnnotatedElementUtils to
  look up annotation attributes for @TransactionConfiguration.
- Implemented missing unit tests for @Rollback resolution in
  TransactionalTestExecutionListener.
- SpringJUnit4ClassRunner now uses AnnotatedElementUtils to look up
  annotation attributes for @Timed.
- TestContextManager now retrieves AnnotationAttributes from
  AnnotationDescriptor to look up annotation attributes for
  @TestExecutionListeners.
- DirtiesContextTestExecutionListener now uses AnnotatedElementUtils to
  look up annotation attributes for @DirtiesContext.

Issue: SPR-11038
This commit is contained in:
Sam Brannen
2013-11-20 19:44:55 +01:00
parent c5779e2ed6
commit 64f593db8f
21 changed files with 1099 additions and 164 deletions

View File

@@ -20,9 +20,9 @@ import java.util.Arrays;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -68,21 +68,29 @@ public class ContextConfigurationAttributes {
* @throws IllegalStateException if both the locations and value attributes have been declared
*/
private static String[] resolveLocations(Class<?> declaringClass, ContextConfiguration contextConfiguration) {
return resolveLocations(declaringClass, contextConfiguration.locations(), contextConfiguration.value());
}
/**
* Resolve resource locations from the supplied {@code locations} and
* {@code value} arrays, which correspond to attributes of the same names in
* the {@link ContextConfiguration} annotation.
*
* @throws IllegalStateException if both the locations and value attributes have been declared
*/
private static String[] resolveLocations(Class<?> declaringClass, String[] locations, String[] value) {
Assert.notNull(declaringClass, "declaringClass must not be null");
String[] locations = contextConfiguration.locations();
String[] valueLocations = contextConfiguration.value();
if (!ObjectUtils.isEmpty(valueLocations) && !ObjectUtils.isEmpty(locations)) {
if (!ObjectUtils.isEmpty(value) && !ObjectUtils.isEmpty(locations)) {
String msg = String.format("Test class [%s] has been configured with @ContextConfiguration's 'value' %s "
+ "and 'locations' %s attributes. Only one declaration of resource "
+ "locations is permitted per @ContextConfiguration annotation.", declaringClass.getName(),
ObjectUtils.nullSafeToString(valueLocations), ObjectUtils.nullSafeToString(locations));
ObjectUtils.nullSafeToString(value), ObjectUtils.nullSafeToString(locations));
logger.error(msg);
throw new IllegalStateException(msg);
}
else if (!ObjectUtils.isEmpty(valueLocations)) {
locations = valueLocations;
else if (!ObjectUtils.isEmpty(value)) {
locations = value;
}
return locations;
@@ -101,6 +109,25 @@ public class ContextConfigurationAttributes {
contextConfiguration.inheritInitializers(), contextConfiguration.name(), contextConfiguration.loader());
}
/**
* Construct a new {@link ContextConfigurationAttributes} instance for the
* supplied {@link ContextConfiguration @ContextConfiguration} annotation and
* the {@linkplain Class test class} that declared it.
* @param declaringClass the test class that declared {@code @ContextConfiguration}
* @param annAttrs the annotation attributes from which to retrieve the attributes
*/
@SuppressWarnings("unchecked")
public ContextConfigurationAttributes(Class<?> declaringClass, AnnotationAttributes annAttrs) {
this(
declaringClass,
resolveLocations(declaringClass, annAttrs.getStringArray("locations"), annAttrs.getStringArray("value")),
annAttrs.getClassArray("classes"),
annAttrs.getBoolean("inheritLocations"),
(Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[]) annAttrs.getClassArray("initializers"),
annAttrs.getBoolean("inheritInitializers"), annAttrs.getString("name"),
(Class<? extends ContextLoader>) annAttrs.getClass("loader"));
}
/**
* Construct a new {@link ContextConfigurationAttributes} instance for the
* {@linkplain Class test class} that declared the

View File

@@ -31,6 +31,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.test.context.MetaAnnotationUtils.AnnotationDescriptor;
import org.springframework.test.context.MetaAnnotationUtils.UntypedAnnotationDescriptor;
@@ -192,9 +193,9 @@ abstract class ContextLoaderUtils {
}
/**
* Convenience method for creating a {@link ContextConfigurationAttributes} instance
* from the supplied {@link ContextConfiguration} and declaring class and then adding
* the attributes to the supplied list.
* Convenience method for creating a {@link ContextConfigurationAttributes}
* instance from the supplied {@link ContextConfiguration} annotation and
* declaring class and then adding the attributes to the supplied list.
*/
private static void convertContextConfigToConfigAttributesAndAddToList(ContextConfiguration contextConfiguration,
Class<?> declaringClass, final List<ContextConfigurationAttributes> attributesList) {
@@ -211,6 +212,27 @@ abstract class ContextLoaderUtils {
attributesList.add(attributes);
}
/**
* Convenience method for creating a {@link ContextConfigurationAttributes}
* instance from the supplied {@link AnnotationAttributes} and declaring
* class and then adding the attributes to the supplied list.
*
* @since 4.0
*/
private static void convertAnnotationAttributesToConfigAttributesAndAddToList(AnnotationAttributes annAttrs,
Class<?> declaringClass, final List<ContextConfigurationAttributes> attributesList) {
if (logger.isTraceEnabled()) {
logger.trace(String.format("Retrieved @ContextConfiguration attributes [%s] for declaring class [%s].",
annAttrs, declaringClass.getName()));
}
ContextConfigurationAttributes attributes = new ContextConfigurationAttributes(declaringClass, annAttrs);
if (logger.isTraceEnabled()) {
logger.trace("Resolved context configuration attributes: " + attributes);
}
attributesList.add(attributes);
}
/**
* Resolve the list of lists of {@linkplain ContextConfigurationAttributes context
* configuration attributes} for the supplied {@linkplain Class test class} and its
@@ -243,6 +265,8 @@ abstract class ContextLoaderUtils {
* <em>present</em> on the supplied class; or if a given class in the class hierarchy
* declares both {@code @ContextConfiguration} and {@code @ContextHierarchy} as
* top-level annotations.
* @throws IllegalStateException if no class in the class hierarchy declares
* {@code @ContextHierarchy}.
*
* @since 3.2.2
* @see #buildContextHierarchyMap(Class)
@@ -251,6 +275,7 @@ abstract class ContextLoaderUtils {
@SuppressWarnings("unchecked")
static List<List<ContextConfigurationAttributes>> resolveContextHierarchyAttributes(Class<?> testClass) {
Assert.notNull(testClass, "Class must not be null");
Assert.state(findAnnotation(testClass, ContextHierarchy.class) != null, "@ContextHierarchy must be present");
final Class<ContextConfiguration> contextConfigType = ContextConfiguration.class;
final Class<ContextHierarchy> contextHierarchyType = ContextHierarchy.class;
@@ -263,17 +288,16 @@ abstract class ContextLoaderUtils {
contextConfigType.getName(), contextHierarchyType.getName(), testClass.getName()));
while (descriptor != null) {
Class<?> rootDeclaringClass = descriptor.getDeclaringClass();
Class<?> declaringClass = (descriptor.getStereotype() != null) ? descriptor.getStereotypeType()
: rootDeclaringClass;
Class<?> rootDeclaringClass = descriptor.getRootDeclaringClass();
Class<?> declaringClass = descriptor.getDeclaringClass();
boolean contextConfigDeclaredLocally = isAnnotationDeclaredLocally(contextConfigType, declaringClass);
boolean contextHierarchyDeclaredLocally = isAnnotationDeclaredLocally(contextHierarchyType, declaringClass);
if (contextConfigDeclaredLocally && contextHierarchyDeclaredLocally) {
String msg = String.format("Test class [%s] has been configured with both @ContextConfiguration "
String msg = String.format("Class [%s] has been configured with both @ContextConfiguration "
+ "and @ContextHierarchy. Only one of these annotations may be declared on a test class "
+ "or custom stereotype annotation.", rootDeclaringClass.getName());
+ "or custom stereotype annotation.", declaringClass.getName());
logger.error(msg);
throw new IllegalStateException(msg);
}
@@ -281,9 +305,8 @@ abstract class ContextLoaderUtils {
final List<ContextConfigurationAttributes> configAttributesList = new ArrayList<ContextConfigurationAttributes>();
if (contextConfigDeclaredLocally) {
ContextConfiguration contextConfiguration = getAnnotation(declaringClass, contextConfigType);
convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass,
configAttributesList);
convertAnnotationAttributesToConfigAttributesAndAddToList(descriptor.getAnnotationAttributes(),
declaringClass, configAttributesList);
}
else if (contextHierarchyDeclaredLocally) {
ContextHierarchy contextHierarchy = getAnnotation(declaringClass, contextHierarchyType);
@@ -293,7 +316,7 @@ abstract class ContextLoaderUtils {
}
}
else {
// This should theoretically actually never happen...
// This should theoretically never happen...
String msg = String.format("Test class [%s] has been configured with neither @ContextConfiguration "
+ "nor @ContextHierarchy as a class-level annotation.", rootDeclaringClass.getName());
logger.error(msg);
@@ -405,13 +428,9 @@ abstract class ContextLoaderUtils {
annotationType.getName(), testClass.getName()));
while (descriptor != null) {
Class<?> rootDeclaringClass = descriptor.getDeclaringClass();
Class<?> declaringClass = (descriptor.getStereotype() != null) ? descriptor.getStereotypeType()
: rootDeclaringClass;
convertContextConfigToConfigAttributesAndAddToList(descriptor.getAnnotation(), declaringClass,
attributesList);
descriptor = findAnnotationDescriptor(rootDeclaringClass.getSuperclass(), annotationType);
convertAnnotationAttributesToConfigAttributesAndAddToList(descriptor.getAnnotationAttributes(),
descriptor.getDeclaringClass(), attributesList);
descriptor = findAnnotationDescriptor(descriptor.getRootDeclaringClass().getSuperclass(), annotationType);
}
return attributesList;
@@ -489,20 +508,18 @@ abstract class ContextLoaderUtils {
final Set<String> activeProfiles = new HashSet<String>();
while (descriptor != null) {
Class<?> rootDeclaringClass = descriptor.getDeclaringClass();
Class<?> declaringClass = (descriptor.getStereotype() != null) ? descriptor.getStereotypeType()
: rootDeclaringClass;
Class<?> declaringClass = descriptor.getDeclaringClass();
ActiveProfiles annotation = descriptor.getAnnotation();
AnnotationAttributes annAttrs = descriptor.getAnnotationAttributes();
if (logger.isTraceEnabled()) {
logger.trace(String.format("Retrieved @ActiveProfiles [%s] for declaring class [%s].", annotation,
declaringClass.getName()));
logger.trace(String.format("Retrieved @ActiveProfiles attributes [%s] for declaring class [%s].",
annAttrs, declaringClass.getName()));
}
validateActiveProfilesConfiguration(declaringClass, annotation);
validateActiveProfilesConfiguration(declaringClass, annAttrs);
String[] profiles = annotation.profiles();
String[] valueProfiles = annotation.value();
Class<? extends ActiveProfilesResolver> resolverClass = annotation.resolver();
String[] profiles = annAttrs.getStringArray("profiles");
String[] valueProfiles = annAttrs.getStringArray("value");
Class<? extends ActiveProfilesResolver> resolverClass = annAttrs.getClass("resolver");
boolean resolverDeclared = !ActiveProfilesResolver.class.equals(resolverClass);
boolean valueDeclared = !ObjectUtils.isEmpty(valueProfiles);
@@ -538,17 +555,17 @@ abstract class ContextLoaderUtils {
}
}
descriptor = annotation.inheritProfiles() ? findAnnotationDescriptor(rootDeclaringClass.getSuperclass(),
annotationType) : null;
descriptor = annAttrs.getBoolean("inheritProfiles") ? findAnnotationDescriptor(
descriptor.getRootDeclaringClass().getSuperclass(), annotationType) : null;
}
return StringUtils.toStringArray(activeProfiles);
}
private static void validateActiveProfilesConfiguration(Class<?> declaringClass, ActiveProfiles annotation) {
String[] valueProfiles = annotation.value();
String[] profiles = annotation.profiles();
Class<? extends ActiveProfilesResolver> resolverClass = annotation.resolver();
private static void validateActiveProfilesConfiguration(Class<?> declaringClass, AnnotationAttributes annAttrs) {
String[] valueProfiles = annAttrs.getStringArray("value");
String[] profiles = annAttrs.getStringArray("profiles");
Class<? extends ActiveProfilesResolver> resolverClass = annAttrs.getClass("resolver");
boolean valueDeclared = !ObjectUtils.isEmpty(valueProfiles);
boolean profilesDeclared = !ObjectUtils.isEmpty(profiles);
boolean resolverDeclared = !ActiveProfilesResolver.class.equals(resolverClass);

View File

@@ -18,6 +18,8 @@ package org.springframework.test.context;
import java.lang.annotation.Annotation;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -124,7 +126,7 @@ abstract class MetaAnnotationUtils {
* <p>
* If the annotation is used as a meta-annotation, the descriptor also includes
* the {@linkplain #getStereotype() stereotype} on which the annotation is
* present. In such cases, the <em>declaring class</em> is not directly
* present. In such cases, the <em>root declaring class</em> is not directly
* annotated with the annotation but rather indirectly via the stereotype.
*
* <p>
@@ -133,6 +135,7 @@ abstract class MetaAnnotationUtils {
* properties of the {@code AnnotationDescriptor} would be as follows.
*
* <ul>
* <li>rootDeclaringClass: {@code TransactionalTests} class object</li>
* <li>declaringClass: {@code TransactionalTests} class object</li>
* <li>stereotype: {@code null}</li>
* <li>annotation: instance of the {@code Transactional} annotation</li>
@@ -150,7 +153,8 @@ abstract class MetaAnnotationUtils {
* properties of the {@code AnnotationDescriptor} would be as follows.
*
* <ul>
* <li>declaringClass: {@code UserRepositoryTests} class object</li>
* <li>rootDeclaringClass: {@code UserRepositoryTests} class object</li>
* <li>declaringClass: {@code RepositoryTests} class object</li>
* <li>stereotype: instance of the {@code RepositoryTests} annotation</li>
* <li>annotation: instance of the {@code Transactional} annotation</li>
* </ul>
@@ -170,22 +174,31 @@ abstract class MetaAnnotationUtils {
*/
public static class AnnotationDescriptor<T extends Annotation> {
private final Class<?> rootDeclaringClass;
private final Class<?> declaringClass;
private final Annotation stereotype;
private final T annotation;
private final AnnotationAttributes annotationAttributes;
public AnnotationDescriptor(Class<?> declaringClass, T annotation) {
this(declaringClass, null, annotation);
public AnnotationDescriptor(Class<?> rootDeclaringClass, T annotation) {
this(rootDeclaringClass, null, annotation);
}
public AnnotationDescriptor(Class<?> declaringClass, Annotation stereotype, T annotation) {
Assert.notNull(declaringClass, "declaringClass must not be null");
public AnnotationDescriptor(Class<?> rootDeclaringClass, Annotation stereotype, T annotation) {
Assert.notNull(rootDeclaringClass, "rootDeclaringClass must not be null");
Assert.notNull(annotation, "annotation must not be null");
this.declaringClass = declaringClass;
this.rootDeclaringClass = rootDeclaringClass;
this.declaringClass = (stereotype != null) ? stereotype.annotationType() : rootDeclaringClass;
this.stereotype = stereotype;
this.annotation = annotation;
this.annotationAttributes = AnnotatedElementUtils.getAnnotationAttributes(rootDeclaringClass,
annotation.annotationType().getName());
}
public Class<?> getRootDeclaringClass() {
return this.rootDeclaringClass;
}
public Class<?> getDeclaringClass() {
@@ -200,6 +213,10 @@ abstract class MetaAnnotationUtils {
return this.annotation.annotationType();
}
public AnnotationAttributes getAnnotationAttributes() {
return this.annotationAttributes;
}
public Annotation getStereotype() {
return this.stereotype;
}
@@ -214,6 +231,7 @@ abstract class MetaAnnotationUtils {
@Override
public String toString() {
return new ToStringCreator(this)//
.append("rootDeclaringClass", rootDeclaringClass)//
.append("declaringClass", declaringClass)//
.append("stereotype", stereotype)//
.append("annotation", annotation)//

View File

@@ -28,6 +28,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.test.context.MetaAnnotationUtils.AnnotationDescriptor;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -172,13 +173,13 @@ public class TestContextManager {
* @param clazz the test class for which the listeners should be retrieved
* @return an array of TestExecutionListeners for the specified class
*/
@SuppressWarnings("unchecked")
private TestExecutionListener[] retrieveTestExecutionListeners(Class<?> clazz) {
Assert.notNull(clazz, "Class must not be null");
Class<TestExecutionListeners> annotationType = TestExecutionListeners.class;
List<Class<? extends TestExecutionListener>> classesList = new ArrayList<Class<? extends TestExecutionListener>>();
AnnotationDescriptor<TestExecutionListeners> descriptor = findAnnotationDescriptor(clazz, annotationType);
boolean defaultListeners = false;
// Use defaults?
@@ -192,21 +193,20 @@ public class TestContextManager {
else {
// Traverse the class hierarchy...
while (descriptor != null) {
Class<?> rootDeclaringClass = descriptor.getDeclaringClass();
Class<?> declaringClass = (descriptor.getStereotype() != null) ? descriptor.getStereotypeType()
: rootDeclaringClass;
Class<?> declaringClass = descriptor.getDeclaringClass();
TestExecutionListeners testExecutionListeners = declaringClass.getAnnotation(annotationType);
AnnotationAttributes annAttrs = descriptor.getAnnotationAttributes();
if (logger.isTraceEnabled()) {
logger.trace("Retrieved @TestExecutionListeners [" + testExecutionListeners
+ "] for declaring class [" + declaringClass + "].");
logger.trace(String.format(
"Retrieved @TestExecutionListeners attributes [%s] for declaring class [%s].", annAttrs,
declaringClass));
}
Class<? extends TestExecutionListener>[] valueListenerClasses = testExecutionListeners.value();
Class<? extends TestExecutionListener>[] listenerClasses = testExecutionListeners.listeners();
Class<? extends TestExecutionListener>[] valueListenerClasses = (Class<? extends TestExecutionListener>[]) annAttrs.getClassArray("value");
Class<? extends TestExecutionListener>[] listenerClasses = (Class<? extends TestExecutionListener>[]) annAttrs.getClassArray("listeners");
if (!ObjectUtils.isEmpty(valueListenerClasses) && !ObjectUtils.isEmpty(listenerClasses)) {
String msg = String.format(
"Test class [%s] has been configured with @TestExecutionListeners' 'value' [%s] "
"Class [%s] has been configured with @TestExecutionListeners' 'value' [%s] "
+ "and 'listeners' [%s] attributes. Use one or the other, but not both.",
declaringClass, ObjectUtils.nullSafeToString(valueListenerClasses),
ObjectUtils.nullSafeToString(listenerClasses));
@@ -221,8 +221,8 @@ public class TestContextManager {
classesList.addAll(0, Arrays.<Class<? extends TestExecutionListener>> asList(listenerClasses));
}
descriptor = (testExecutionListeners.inheritListeners() ? findAnnotationDescriptor(
rootDeclaringClass.getSuperclass(), annotationType) : null);
descriptor = (annAttrs.getBoolean("inheritListeners") ? findAnnotationDescriptor(
descriptor.getRootDeclaringClass().getSuperclass(), annotationType) : null);
}
}

View File

@@ -34,6 +34,8 @@ import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.test.annotation.ProfileValueUtils;
import org.springframework.test.annotation.Repeat;
@@ -411,8 +413,15 @@ public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner {
* @return the timeout, or {@code 0} if none was specified.
*/
protected long getSpringTimeout(FrameworkMethod frameworkMethod) {
Timed timedAnnotation = AnnotationUtils.getAnnotation(frameworkMethod.getMethod(), Timed.class);
return (timedAnnotation != null && timedAnnotation.millis() > 0 ? timedAnnotation.millis() : 0);
AnnotationAttributes annAttrs = AnnotatedElementUtils.getAnnotationAttributes(frameworkMethod.getMethod(),
Timed.class.getName());
if (annAttrs == null) {
return 0;
}
else {
long millis = annAttrs.<Long> getNumber("millis").longValue();
return millis > 0 ? millis : 0;
}
}
/**

View File

@@ -21,13 +21,15 @@ import java.lang.reflect.Method;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotatedElementUtils;
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.context.TestContext;
import org.springframework.util.Assert;
import static org.springframework.core.annotation.AnnotationUtils.*;
import static org.springframework.test.annotation.DirtiesContext.ClassMode.*;
/**
* {@code TestExecutionListener} which provides support for marking the
@@ -81,24 +83,22 @@ public class DirtiesContextTestExecutionListener extends AbstractTestExecutionLi
Method testMethod = testContext.getTestMethod();
Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");
final Class<DirtiesContext> annotationType = DirtiesContext.class;
DirtiesContext methodDirtiesContextAnnotation = findAnnotation(testMethod, annotationType);
boolean methodDirtiesContext = methodDirtiesContextAnnotation != null;
DirtiesContext classDirtiesContextAnnotation = findAnnotation(testClass, annotationType);
boolean classDirtiesContext = classDirtiesContextAnnotation != null;
ClassMode classMode = classDirtiesContext ? classDirtiesContextAnnotation.classMode() : 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("After test method: context [" + testContext + "], class dirties context ["
+ classDirtiesContext + "], class mode [" + classMode + "], method dirties context ["
+ methodDirtiesContext + "].");
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 == ClassMode.AFTER_EACH_TEST_METHOD)) {
HierarchyMode hierarchyMode = methodDirtiesContext ? methodDirtiesContextAnnotation.hierarchyMode()
: classDirtiesContextAnnotation.hierarchyMode();
if (methodDirtiesContext || (classMode == AFTER_EACH_TEST_METHOD)) {
HierarchyMode hierarchyMode = methodDirtiesContext ? methodAnnAttrs.<HierarchyMode> getEnum("hierarchyMode")
: classAnnAttrs.<HierarchyMode> getEnum("hierarchyMode");
dirtyContext(testContext, hierarchyMode);
}
}
@@ -107,7 +107,7 @@ public class DirtiesContextTestExecutionListener extends AbstractTestExecutionLi
* 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} ,
* 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
@@ -118,15 +118,16 @@ public class DirtiesContextTestExecutionListener extends AbstractTestExecutionLi
Class<?> testClass = testContext.getTestClass();
Assert.notNull(testClass, "The test class of the supplied TestContext must not be null");
final Class<DirtiesContext> annotationType = DirtiesContext.class;
final String annotationType = DirtiesContext.class.getName();
AnnotationAttributes annAttrs = AnnotatedElementUtils.getAnnotationAttributes(testClass, annotationType);
boolean dirtiesContext = annAttrs != null;
DirtiesContext dirtiesContextAnnotation = findAnnotation(testClass, annotationType);
boolean dirtiesContext = dirtiesContextAnnotation != null;
if (logger.isDebugEnabled()) {
logger.debug("After test class: context [" + testContext + "], dirtiesContext [" + dirtiesContext + "].");
logger.debug(String.format("After test class: context %s, dirtiesContext [%s].", testContext,
dirtiesContext));
}
if (dirtiesContext) {
HierarchyMode hierarchyMode = dirtiesContextAnnotation.hierarchyMode();
HierarchyMode hierarchyMode = annAttrs.<HierarchyMode> getEnum("hierarchyMode");
dirtyContext(testContext, hierarchyMode);
}
}

View File

@@ -33,6 +33,8 @@ import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;
@@ -414,15 +416,17 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
if (rollbackAnnotation != null) {
boolean rollbackOverride = rollbackAnnotation.value();
if (logger.isDebugEnabled()) {
logger.debug("Method-level @Rollback(" + rollbackOverride + ") overrides default rollback [" + rollback
+ "] for test context " + testContext);
logger.debug(String.format(
"Method-level @Rollback(%s) overrides default rollback [%s] for test context %s.",
rollbackOverride, rollback, testContext));
}
rollback = rollbackOverride;
}
else {
if (logger.isDebugEnabled()) {
logger.debug("No method-level @Rollback override: using default rollback [" + rollback
+ "] for test context " + testContext);
logger.debug(String.format(
"No method-level @Rollback override: using default rollback [%s] for test context %s.", rollback,
testContext));
}
}
return rollback;
@@ -524,21 +528,25 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
* {@code @TransactionConfiguration} will be used instead.
* @param testContext the test context for which the configuration
* attributes should be retrieved
* @return a new TransactionConfigurationAttributes instance
* @return the TransactionConfigurationAttributes instance for this listener,
* potentially cached
*/
private TransactionConfigurationAttributes retrieveConfigurationAttributes(TestContext testContext) {
TransactionConfigurationAttributes retrieveConfigurationAttributes(TestContext testContext) {
if (this.configurationAttributes == null) {
Class<?> clazz = testContext.getTestClass();
TransactionConfiguration config = findAnnotation(clazz, TransactionConfiguration.class);
AnnotationAttributes annAttrs = AnnotatedElementUtils.getAnnotationAttributes(clazz,
TransactionConfiguration.class.getName());
if (logger.isDebugEnabled()) {
logger.debug("Retrieved @TransactionConfiguration [" + config + "] for test class [" + clazz + "]");
logger.debug(String.format("Retrieved @TransactionConfiguration attributes [%s] for test class [%s].",
annAttrs, clazz));
}
String transactionManagerName;
boolean defaultRollback;
if (config != null) {
transactionManagerName = config.transactionManager();
defaultRollback = config.defaultRollback();
if (annAttrs != null) {
transactionManagerName = annAttrs.getString("transactionManager");
defaultRollback = annAttrs.getBoolean("defaultRollback");
}
else {
transactionManagerName = DEFAULT_TRANSACTION_MANAGER_NAME;
@@ -548,8 +556,8 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
TransactionConfigurationAttributes configAttributes = new TransactionConfigurationAttributes(
transactionManagerName, defaultRollback);
if (logger.isDebugEnabled()) {
logger.debug("Retrieved TransactionConfigurationAttributes " + configAttributes + " for class ["
+ clazz + "]");
logger.debug(String.format("Retrieved TransactionConfigurationAttributes %s for class [%s].",
configAttributes, clazz));
}
this.configurationAttributes = configAttributes;
}