Provide meta-annotation support in the TCF

Spring 3.0 already allows component stereotypes to be used in a
meta-annotation fashion, for example by creating a custom
@TransactionalService stereotype annotation which combines
@Transactional and @Service in a single, reusable, application-specific
annotation. However, the Spring TestContext Framework (TCF) currently
does not provide any support for test-related annotations to be used as
meta-annotations.

This commit overhauls the TCF with regard to how annotations are
retrieved and adds explicit support for the following annotations to be
used as meta-annotations in conjunction with the TCF.

- @ContextConfiguration
- @ContextHierarchy
- @ActiveProfiles
- @DirtiesContext
- @IfProfileValue
- @ProfileValueSourceConfiguration
- @BeforeTransaction
- @AfterTransaction
- @TransactionConfiguration
- @Rollback
- @TestExecutionListeners
- @Repeat
- @Timed
- @WebAppConfiguration

Note that meta-annotation support for @Transactional was already
available prior to this commit.

The following is a summary of the major changes included in this commit.

- Now using AnnotationUtils.getAnnotation() instead of
  Class.getAnnotation() where appropriate in the TestContext Framework.
- Now using AnnotationUtils.findAnnotation() instead of
  Class.isAnnotationPresent() where appropriate in the TestContext
  Framework.
- Introduced findAnnotationPrefersInteracesOverLocalMetaAnnotations() in
  AnnotationUtilsTests in order to verify the status quo.
- AnnotationUtils.findAnnotationDeclaringClass() and
  AnnotationUtils.findAnnotationDeclaringClassForTypes() now support
  meta annotations.
- Introduced MetaAnnotationUtils and AnnotationDescriptor in the
  spring-test module.
- Introduced UntypedAnnotationDescriptor in MetaAnnotationUtils.
- Introduced findAnnotationDescriptorForTypes() in MetaAnnotationUtils.
- ContextLoaderUtils now uses MetaAnnotationUtils for looking up
  @ActiveProfiles as a potential meta-annotation.
- TestContextManager now uses MetaAnnotationUtils for looking up
  @TestExecutionListeners as a potential meta-annotation.
- DirtiesContextTestExecutionListener now uses AnnotationUtils for
  looking up @DirtiesContext as a potential meta-annotation.
- Introduced DirtiesContextTestExecutionListenerTests.
- ProfileValueUtils now uses AnnotationUtils for looking up
  @IfProfileValue and @ProfileValueSourceConfiguration as potential
  meta-annotations.
- @BeforeTransaction and @AfterTransaction now support ANNOTATION_TYPE
  as a target, allowing them to be used as meta-annotations.
- TransactionalTestExecutionListener now uses AnnotationUtils for
  looking up @BeforeTransaction, @AfterTransaction, @Rollback, and
  @TransactionConfiguration as potential meta-annotations.
- Introduced TransactionalTestExecutionListenerTests.
- @Repeat and @Timed now support ANNOTATION_TYPE as a target, allowing
  them to be used as meta-annotations.
- SpringJUnit4ClassRunner now uses AnnotationUtils for looking up
  @Repeat and @Timed as potential meta-annotations.
- Moved all remaining logic for building the MergedContextConfiguration
  from the DefaultTestContext constructor to
  ContextLoaderUtils.buildMergedContextConfiguration().
- Verified meta-annotation support for @WebAppConfiguration and
  @ContextConfiguration.

Issue: SPR-7827
This commit is contained in:
Sam Brannen
2013-07-01 23:01:31 -04:00
parent 56dfcd153e
commit 5e7021f3f7
28 changed files with 1852 additions and 205 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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.
@@ -25,6 +25,8 @@ import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import static org.springframework.core.annotation.AnnotationUtils.*;
/**
* General utility methods for working with <em>profile values</em>.
*
@@ -63,7 +65,7 @@ public abstract class ProfileValueUtils {
Assert.notNull(testClass, "testClass must not be null");
Class<ProfileValueSourceConfiguration> annotationType = ProfileValueSourceConfiguration.class;
ProfileValueSourceConfiguration config = testClass.getAnnotation(annotationType);
ProfileValueSourceConfiguration config = findAnnotation(testClass, annotationType);
if (logger.isDebugEnabled()) {
logger.debug("Retrieved @ProfileValueSourceConfiguration [" + config + "] for test class ["
+ testClass.getName() + "]");
@@ -114,7 +116,7 @@ public abstract class ProfileValueUtils {
* environment
*/
public static boolean isTestEnabledInThisEnvironment(Class<?> testClass) {
IfProfileValue ifProfileValue = testClass.getAnnotation(IfProfileValue.class);
IfProfileValue ifProfileValue = findAnnotation(testClass, IfProfileValue.class);
return isTestEnabledInThisEnvironment(retrieveProfileValueSource(testClass), ifProfileValue);
}
@@ -157,11 +159,11 @@ public abstract class ProfileValueUtils {
public static boolean isTestEnabledInThisEnvironment(ProfileValueSource profileValueSource, Method testMethod,
Class<?> testClass) {
IfProfileValue ifProfileValue = testClass.getAnnotation(IfProfileValue.class);
IfProfileValue ifProfileValue = findAnnotation(testClass, IfProfileValue.class);
boolean classLevelEnabled = isTestEnabledInThisEnvironment(profileValueSource, ifProfileValue);
if (classLevelEnabled) {
ifProfileValue = testMethod.getAnnotation(IfProfileValue.class);
ifProfileValue = findAnnotation(testMethod, IfProfileValue.class);
return isTestEnabledInThisEnvironment(profileValueSource, ifProfileValue);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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.
@@ -17,25 +17,27 @@
package org.springframework.test.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
/**
* Test annotation to indicate that a test method should be invoked repeatedly.
* <p />
* Note that the scope of execution to be repeated includes execution of the
*
* <p>Note that the scope of execution to be repeated includes execution of the
* test method itself as well as any <em>set up</em> or <em>tear down</em> of
* the test fixture.
*
* @author Rod Johnson
* @author Sam Brannen
* @since 2.0
* @see Timed
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Retention(RUNTIME)
@Target({ METHOD, ANNOTATION_TYPE })
public @interface Repeat {
int value() default 1;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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.
@@ -17,25 +17,22 @@
package org.springframework.test.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
/**
* <p>
* Test-specific annotation to indicate that a test method has to finish
* execution in a {@link #millis() specified time period}.
* </p>
* <p>
* If the text execution takes longer than the specified time period, then the
* test is to be considered failed.
* </p>
* <p>
* Note that the time period includes execution of the test method itself, any
* {@link Repeat repetitions} of the test, and any <em>set up</em> or
*
* <p>If the text execution takes longer than the specified time period, then
* the test is to be considered failed.
*
* <p>Note that the time period includes execution of the test method itself,
* any {@link Repeat repetitions} of the test, and any <em>set up</em> or
* <em>tear down</em> of the test fixture.
* </p>
*
* @author Rod Johnson
* @author Sam Brannen
@@ -43,8 +40,8 @@ import java.lang.annotation.Target;
* @see Repeat
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Retention(RUNTIME)
@Target({ METHOD, ANNOTATION_TYPE })
public @interface Timed {
/**

View File

@@ -32,6 +32,8 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.test.context.MetaAnnotationUtils.AnnotationDescriptor;
import org.springframework.test.context.MetaAnnotationUtils.UntypedAnnotationDescriptor;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
@@ -39,6 +41,7 @@ import org.springframework.util.StringUtils;
import static org.springframework.beans.BeanUtils.*;
import static org.springframework.core.annotation.AnnotationUtils.*;
import static org.springframework.test.context.MetaAnnotationUtils.*;
/**
* Utility methods for working with {@link ContextLoader ContextLoaders} and
@@ -108,7 +111,7 @@ abstract class ContextLoaderUtils {
if (!StringUtils.hasText(defaultContextLoaderClassName)) {
Class<? extends Annotation> webAppConfigClass = loadWebAppConfigurationClass();
defaultContextLoaderClassName = webAppConfigClass != null
&& testClass.isAnnotationPresent(webAppConfigClass) ? DEFAULT_WEB_CONTEXT_LOADER_CLASS_NAME
&& findAnnotation(testClass, webAppConfigClass) != null ? DEFAULT_WEB_CONTEXT_LOADER_CLASS_NAME
: DEFAULT_CONTEXT_LOADER_CLASS_NAME;
}
@@ -245,29 +248,32 @@ abstract class ContextLoaderUtils {
* @see #buildContextHierarchyMap(Class)
* @see #resolveContextConfigurationAttributes(Class)
*/
@SuppressWarnings("unchecked")
static List<List<ContextConfigurationAttributes>> resolveContextHierarchyAttributes(Class<?> testClass) {
Assert.notNull(testClass, "Class must not be null");
final Class<ContextConfiguration> contextConfigType = ContextConfiguration.class;
final Class<ContextHierarchy> contextHierarchyType = ContextHierarchy.class;
final List<Class<? extends Annotation>> annotationTypes = Arrays.asList(contextConfigType, contextHierarchyType);
final List<List<ContextConfigurationAttributes>> hierarchyAttributes = new ArrayList<List<ContextConfigurationAttributes>>();
Class<?> declaringClass = findAnnotationDeclaringClassForTypes(annotationTypes, testClass);
Assert.notNull(declaringClass, String.format(
UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(testClass, contextConfigType,
contextHierarchyType);
Assert.notNull(descriptor, String.format(
"Could not find an 'annotation declaring class' for annotation type [%s] or [%s] and test class [%s]",
contextConfigType.getName(), contextHierarchyType.getName(), testClass.getName()));
while (declaringClass != null) {
while (descriptor != null) {
Class<?> rootDeclaringClass = descriptor.getDeclaringClass();
Class<?> declaringClass = (descriptor.getStereotype() != null) ? descriptor.getStereotypeType()
: rootDeclaringClass;
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 "
+ "and @ContextHierarchy as class-level annotations. Only one of these annotations may "
+ "be declared as a top-level annotation per test class.", declaringClass.getName());
+ "and @ContextHierarchy. Only one of these annotations may be declared on a test class "
+ "or custom stereotype annotation.", rootDeclaringClass.getName());
logger.error(msg);
throw new IllegalStateException(msg);
}
@@ -275,12 +281,12 @@ abstract class ContextLoaderUtils {
final List<ContextConfigurationAttributes> configAttributesList = new ArrayList<ContextConfigurationAttributes>();
if (contextConfigDeclaredLocally) {
ContextConfiguration contextConfiguration = declaringClass.getAnnotation(contextConfigType);
ContextConfiguration contextConfiguration = getAnnotation(declaringClass, contextConfigType);
convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass,
configAttributesList);
}
else if (contextHierarchyDeclaredLocally) {
ContextHierarchy contextHierarchy = declaringClass.getAnnotation(contextHierarchyType);
ContextHierarchy contextHierarchy = getAnnotation(declaringClass, contextHierarchyType);
for (ContextConfiguration contextConfiguration : contextHierarchy.value()) {
convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass,
configAttributesList);
@@ -289,14 +295,15 @@ abstract class ContextLoaderUtils {
else {
// This should theoretically actually never happen...
String msg = String.format("Test class [%s] has been configured with neither @ContextConfiguration "
+ "nor @ContextHierarchy as a class-level annotation.", declaringClass.getName());
+ "nor @ContextHierarchy as a class-level annotation.", rootDeclaringClass.getName());
logger.error(msg);
throw new IllegalStateException(msg);
}
hierarchyAttributes.add(0, configAttributesList);
declaringClass = findAnnotationDeclaringClassForTypes(annotationTypes, declaringClass.getSuperclass());
descriptor = findAnnotationDescriptorForTypes(rootDeclaringClass.getSuperclass(), contextConfigType,
contextHierarchyType);
}
return hierarchyAttributes;
@@ -391,15 +398,20 @@ abstract class ContextLoaderUtils {
final List<ContextConfigurationAttributes> attributesList = new ArrayList<ContextConfigurationAttributes>();
Class<ContextConfiguration> annotationType = ContextConfiguration.class;
Class<?> declaringClass = findAnnotationDeclaringClass(annotationType, testClass);
Assert.notNull(declaringClass, String.format(
AnnotationDescriptor<ContextConfiguration> descriptor = findAnnotationDescriptor(testClass, annotationType);
Assert.notNull(descriptor, String.format(
"Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]",
annotationType.getName(), testClass.getName()));
while (declaringClass != null) {
ContextConfiguration contextConfiguration = declaringClass.getAnnotation(annotationType);
convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, declaringClass, attributesList);
declaringClass = findAnnotationDeclaringClass(annotationType, declaringClass.getSuperclass());
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);
}
return attributesList;
@@ -465,9 +477,10 @@ abstract class ContextLoaderUtils {
Assert.notNull(testClass, "Class must not be null");
Class<ActiveProfiles> annotationType = ActiveProfiles.class;
Class<?> declaringClass = findAnnotationDeclaringClass(annotationType, testClass);
if (declaringClass == null && logger.isDebugEnabled()) {
AnnotationDescriptor<ActiveProfiles> descriptor = findAnnotationDescriptor(testClass, annotationType);
if (descriptor == null && logger.isDebugEnabled()) {
logger.debug(String.format(
"Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]",
annotationType.getName(), testClass.getName()));
@@ -475,8 +488,12 @@ abstract class ContextLoaderUtils {
final Set<String> activeProfiles = new HashSet<String>();
while (declaringClass != null) {
ActiveProfiles annotation = declaringClass.getAnnotation(annotationType);
while (descriptor != null) {
Class<?> rootDeclaringClass = descriptor.getDeclaringClass();
Class<?> declaringClass = (descriptor.getStereotype() != null) ? descriptor.getStereotypeType()
: rootDeclaringClass;
ActiveProfiles annotation = descriptor.getAnnotation();
if (logger.isTraceEnabled()) {
logger.trace(String.format("Retrieved @ActiveProfiles [%s] for declaring class [%s].", annotation,
declaringClass.getName()));
@@ -521,8 +538,8 @@ abstract class ContextLoaderUtils {
}
}
declaringClass = annotation.inheritProfiles() ? findAnnotationDeclaringClass(annotationType,
declaringClass.getSuperclass()) : null;
descriptor = annotation.inheritProfiles() ? findAnnotationDescriptor(rootDeclaringClass.getSuperclass(),
annotationType) : null;
}
return StringUtils.toStringArray(activeProfiles);
@@ -582,11 +599,20 @@ abstract class ContextLoaderUtils {
* @see #buildContextHierarchyMap(Class)
* @see #buildMergedContextConfiguration(Class, List, String, MergedContextConfiguration, CacheAwareContextLoaderDelegate)
*/
@SuppressWarnings("javadoc")
@SuppressWarnings({ "javadoc", "unchecked" })
static MergedContextConfiguration buildMergedContextConfiguration(Class<?> testClass,
String defaultContextLoaderClassName, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {
if (testClass.isAnnotationPresent(ContextHierarchy.class)) {
if (findAnnotationDescriptorForTypes(testClass, ContextConfiguration.class, ContextHierarchy.class) == null) {
if (logger.isInfoEnabled()) {
logger.info(String.format(
"Neither @ContextConfiguration nor @ContextHierarchy found for test class [%s]",
testClass.getName()));
}
return new MergedContextConfiguration(testClass, null, null, null, null);
}
if (findAnnotation(testClass, ContextHierarchy.class) != null) {
Map<String, List<ContextConfigurationAttributes>> hierarchyMap = buildContextHierarchyMap(testClass);
MergedContextConfiguration parentConfig = null;
@@ -729,28 +755,32 @@ abstract class ContextLoaderUtils {
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parentConfig) {
Class<? extends Annotation> webAppConfigClass = loadWebAppConfigurationClass();
if (webAppConfigClass != null) {
if (webAppConfigClass != null && testClass.isAnnotationPresent(webAppConfigClass)) {
Annotation annotation = testClass.getAnnotation(webAppConfigClass);
String resourceBasePath = (String) AnnotationUtils.getValue(annotation);
Annotation annotation = findAnnotation(testClass, webAppConfigClass);
if (annotation != null) {
try {
Class<? extends MergedContextConfiguration> webMergedConfigClass = (Class<? extends MergedContextConfiguration>) ClassUtils.forName(
WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME, ContextLoaderUtils.class.getClassLoader());
String resourceBasePath = (String) AnnotationUtils.getValue(annotation);
Constructor<? extends MergedContextConfiguration> constructor = ClassUtils.getConstructorIfAvailable(
webMergedConfigClass, Class.class, String[].class, Class[].class, Set.class, String[].class,
String.class, ContextLoader.class, CacheAwareContextLoaderDelegate.class,
MergedContextConfiguration.class);
try {
Class<? extends MergedContextConfiguration> webMergedConfigClass = (Class<? extends MergedContextConfiguration>) ClassUtils.forName(
WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME, ContextLoaderUtils.class.getClassLoader());
if (constructor != null) {
return instantiateClass(constructor, testClass, locations, classes, initializerClasses,
activeProfiles, resourceBasePath, contextLoader, cacheAwareContextLoaderDelegate, parentConfig);
Constructor<? extends MergedContextConfiguration> constructor = ClassUtils.getConstructorIfAvailable(
webMergedConfigClass, Class.class, String[].class, Class[].class, Set.class, String[].class,
String.class, ContextLoader.class, CacheAwareContextLoaderDelegate.class,
MergedContextConfiguration.class);
if (constructor != null) {
return instantiateClass(constructor, testClass, locations, classes, initializerClasses,
activeProfiles, resourceBasePath, contextLoader, cacheAwareContextLoaderDelegate,
parentConfig);
}
}
}
catch (Throwable t) {
if (logger.isDebugEnabled()) {
logger.debug("Could not instantiate [" + WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME + "].", t);
catch (Throwable t) {
if (logger.isDebugEnabled()) {
logger.debug("Could not instantiate [" + WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME + "].", t);
}
}
}
}

View File

@@ -18,9 +18,6 @@ package org.springframework.test.context;
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.AttributeAccessorSupport;
import org.springframework.core.style.ToStringCreator;
@@ -43,8 +40,6 @@ class DefaultTestContext extends AttributeAccessorSupport implements TestContext
private static final long serialVersionUID = -5827157174866681233L;
private static final Log logger = LogFactory.getLog(DefaultTestContext.class);
private final ContextCache contextCache;
private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate;
@@ -95,24 +90,8 @@ class DefaultTestContext extends AttributeAccessorSupport implements TestContext
this.testClass = testClass;
this.contextCache = contextCache;
this.cacheAwareContextLoaderDelegate = new CacheAwareContextLoaderDelegate(contextCache);
MergedContextConfiguration mergedContextConfiguration;
if (testClass.isAnnotationPresent(ContextConfiguration.class)
|| testClass.isAnnotationPresent(ContextHierarchy.class)) {
mergedContextConfiguration = ContextLoaderUtils.buildMergedContextConfiguration(testClass,
defaultContextLoaderClassName, cacheAwareContextLoaderDelegate);
}
else {
if (logger.isInfoEnabled()) {
logger.info(String.format(
"Neither @ContextConfiguration nor @ContextHierarchy found for test class [%s]",
testClass.getName()));
}
mergedContextConfiguration = new MergedContextConfiguration(testClass, null, null, null, null);
}
this.mergedContextConfiguration = mergedContextConfiguration;
this.mergedContextConfiguration = ContextLoaderUtils.buildMergedContextConfiguration(testClass,
defaultContextLoaderClassName, cacheAwareContextLoaderDelegate);
}
/**

View File

@@ -0,0 +1,248 @@
/*
* Copyright 2002-2013 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
*
* http://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;
import java.lang.annotation.Annotation;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import static org.springframework.core.annotation.AnnotationUtils.*;
/**
* TODO Document MetaAnnotationUtils.
*
* @author Sam Brannen
* @since 4.0
*/
abstract class MetaAnnotationUtils {
private MetaAnnotationUtils() {
/* no-op */
}
/**
* TODO Document findAnnotationDescriptor().
*
* @param clazz the class to look for annotations on
* @param annotationType the annotation class to look for, both locally and
* as a meta-annotation
* @return the corresponding annotation descriptor if the annotation was found;
* otherwise {@code null}
*/
public static <T extends Annotation> AnnotationDescriptor<T> findAnnotationDescriptor(Class<?> clazz,
Class<T> annotationType) {
Assert.notNull(annotationType, "Annotation type must not be null");
if (clazz == null || clazz.equals(Object.class)) {
return null;
}
// Declared locally?
if (isAnnotationDeclaredLocally(annotationType, clazz)) {
return new AnnotationDescriptor<T>(clazz, clazz.getAnnotation(annotationType));
}
// Declared on a stereotype annotation (i.e., as a meta-annotation)?
if (!Annotation.class.isAssignableFrom(clazz)) {
for (Annotation stereotype : clazz.getAnnotations()) {
T annotation = stereotype.annotationType().getAnnotation(annotationType);
if (annotation != null) {
return new AnnotationDescriptor<T>(clazz, stereotype, annotation);
}
}
}
// Declared on a superclass?
return findAnnotationDescriptor(clazz.getSuperclass(), annotationType);
}
/**
* TODO Document findAnnotationDescriptorForTypes().
*
* @param clazz the class to look for annotations on
* @param annotationTypes the types of annotations to look for, both locally
* and as meta-annotations
* @return the corresponding annotation descriptor if one of the annotations
* was found; otherwise {@code null}
*/
@SuppressWarnings("unchecked")
public static UntypedAnnotationDescriptor findAnnotationDescriptorForTypes(Class<?> clazz,
Class<? extends Annotation>... annotationTypes) {
assertNonEmptyAnnotationTypeArray(annotationTypes, "The list of annotation types must not be empty");
if (clazz == null || clazz.equals(Object.class)) {
return null;
}
// Declared locally?
for (Class<? extends Annotation> annotationType : annotationTypes) {
if (isAnnotationDeclaredLocally(annotationType, clazz)) {
return new UntypedAnnotationDescriptor(clazz, clazz.getAnnotation(annotationType));
}
}
// Declared on a stereotype annotation (i.e., as a meta-annotation)?
if (!Annotation.class.isAssignableFrom(clazz)) {
for (Annotation stereotype : clazz.getAnnotations()) {
for (Class<? extends Annotation> annotationType : annotationTypes) {
Annotation annotation = stereotype.annotationType().getAnnotation(annotationType);
if (annotation != null) {
return new UntypedAnnotationDescriptor(clazz, stereotype, annotation);
}
}
}
}
// Declared on a superclass?
return findAnnotationDescriptorForTypes(clazz.getSuperclass(), annotationTypes);
}
/**
* Descriptor for an {@link Annotation}, including the {@linkplain
* #getDeclaringClass() class} on which the annotation is <em>declared</em>
* as well as the actual {@linkplain #getAnnotation() annotation} instance.
*
* <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
* annotated with the annotation but rather indirectly via the stereotype.
*
* <p>
* Given the following example, if we are searching for the {@code @Transactional}
* annotation <em>on</em> the {@code TransactionalTests} class, then the
* properties of the {@code AnnotationDescriptor} would be as follows.
*
* <ul>
* <li>declaringClass: {@code TransactionalTests} class object</li>
* <li>stereotype: {@code null}</li>
* <li>annotation: instance of the {@code Transactional} annotation</li>
* </ul>
*
* <pre style="code">
* &#064;Transactional
* &#064;ContextConfiguration({"/test-datasource.xml", "/repository-config.xml"})
* public class TransactionalTests { }
* </pre>
*
* <p>
* Given the following example, if we are searching for the {@code @Transactional}
* annotation <em>on</em> the {@code UserRepositoryTests} class, then the
* properties of the {@code AnnotationDescriptor} would be as follows.
*
* <ul>
* <li>declaringClass: {@code UserRepositoryTests} class object</li>
* <li>stereotype: instance of the {@code RepositoryTests} annotation</li>
* <li>annotation: instance of the {@code Transactional} annotation</li>
* </ul>
*
* <pre style="code">
* &#064;Transactional
* &#064;ContextConfiguration({"/test-datasource.xml", "/repository-config.xml"})
* &#064;Retention(RetentionPolicy.RUNTIME)
* public &#064;interface RepositoryTests { }
*
* &#064;RepositoryTests
* public class UserRepositoryTests { }
* </pre>
*
* @author Sam Brannen
* @since 4.0
*/
public static class AnnotationDescriptor<T extends Annotation> {
private final Class<?> declaringClass;
private final Annotation stereotype;
private final T annotation;
public AnnotationDescriptor(Class<?> declaringClass, T annotation) {
this(declaringClass, null, annotation);
}
public AnnotationDescriptor(Class<?> declaringClass, Annotation stereotype, T annotation) {
Assert.notNull(declaringClass, "declaringClass must not be null");
Assert.notNull(annotation, "annotation must not be null");
this.declaringClass = declaringClass;
this.stereotype = stereotype;
this.annotation = annotation;
}
public Class<?> getDeclaringClass() {
return this.declaringClass;
}
public T getAnnotation() {
return this.annotation;
}
public Class<? extends Annotation> getAnnotationType() {
return this.annotation.annotationType();
}
public Annotation getStereotype() {
return this.stereotype;
}
public Class<? extends Annotation> getStereotypeType() {
return this.stereotype == null ? null : this.stereotype.annotationType();
}
/**
* Provide a textual representation of this {@code AnnotationDescriptor}.
*/
@Override
public String toString() {
return new ToStringCreator(this)//
.append("declaringClass", declaringClass)//
.append("stereotype", stereotype)//
.append("annotation", annotation)//
.toString();
}
}
public static class UntypedAnnotationDescriptor extends AnnotationDescriptor<Annotation> {
public UntypedAnnotationDescriptor(Class<?> declaringClass, Annotation annotation) {
super(declaringClass, annotation);
}
public UntypedAnnotationDescriptor(Class<?> declaringClass, Annotation stereotype, Annotation annotation) {
super(declaringClass, stereotype, annotation);
}
}
private static void assertNonEmptyAnnotationTypeArray(Class<?>[] annotationTypes, String message) {
if (ObjectUtils.isEmpty(annotationTypes)) {
throw new IllegalArgumentException(message);
}
for (Class clazz : annotationTypes) {
if (!Annotation.class.isAssignableFrom(clazz)) {
throw new IllegalArgumentException("Array elements must be of type Annotation");
}
}
}
}

View File

@@ -28,10 +28,12 @@ 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.AnnotationUtils;
import org.springframework.test.context.MetaAnnotationUtils.AnnotationDescriptor;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import static org.springframework.test.context.MetaAnnotationUtils.*;
/**
* <p>
* {@code TestContextManager} is the main entry point into the
@@ -174,11 +176,13 @@ public class TestContextManager {
Assert.notNull(clazz, "Class must not be null");
Class<TestExecutionListeners> annotationType = TestExecutionListeners.class;
List<Class<? extends TestExecutionListener>> classesList = new ArrayList<Class<? extends TestExecutionListener>>();
Class<?> declaringClass = AnnotationUtils.findAnnotationDeclaringClass(annotationType, clazz);
AnnotationDescriptor<TestExecutionListeners> descriptor = findAnnotationDescriptor(clazz, annotationType);
boolean defaultListeners = false;
// Use defaults?
if (declaringClass == null) {
if (descriptor == null) {
if (logger.isDebugEnabled()) {
logger.debug("@TestExecutionListeners is not present for class [" + clazz + "]: using defaults.");
}
@@ -187,7 +191,11 @@ public class TestContextManager {
}
else {
// Traverse the class hierarchy...
while (declaringClass != null) {
while (descriptor != null) {
Class<?> rootDeclaringClass = descriptor.getDeclaringClass();
Class<?> declaringClass = (descriptor.getStereotype() != null) ? descriptor.getStereotypeType()
: rootDeclaringClass;
TestExecutionListeners testExecutionListeners = declaringClass.getAnnotation(annotationType);
if (logger.isTraceEnabled()) {
logger.trace("Retrieved @TestExecutionListeners [" + testExecutionListeners
@@ -212,8 +220,9 @@ public class TestContextManager {
if (listenerClasses != null) {
classesList.addAll(0, Arrays.<Class<? extends TestExecutionListener>> asList(listenerClasses));
}
declaringClass = (testExecutionListeners.inheritListeners() ? AnnotationUtils.findAnnotationDeclaringClass(
annotationType, declaringClass.getSuperclass()) : null);
descriptor = (testExecutionListeners.inheritListeners() ? findAnnotationDescriptor(
rootDeclaringClass.getSuperclass(), annotationType) : null);
}
}

View File

@@ -34,6 +34,7 @@ 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.AnnotationUtils;
import org.springframework.test.annotation.ProfileValueUtils;
import org.springframework.test.annotation.Repeat;
import org.springframework.test.annotation.Timed;
@@ -410,7 +411,7 @@ public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner {
* @return the timeout, or {@code 0} if none was specified.
*/
protected long getSpringTimeout(FrameworkMethod frameworkMethod) {
Timed timedAnnotation = frameworkMethod.getAnnotation(Timed.class);
Timed timedAnnotation = AnnotationUtils.getAnnotation(frameworkMethod.getMethod(), Timed.class);
return (timedAnnotation != null && timedAnnotation.millis() > 0 ? timedAnnotation.millis() : 0);
}
@@ -449,7 +450,7 @@ public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner {
* @see SpringRepeat
*/
protected Statement withPotentialRepeat(FrameworkMethod frameworkMethod, Object testInstance, Statement next) {
Repeat repeatAnnotation = frameworkMethod.getAnnotation(Repeat.class);
Repeat repeatAnnotation = AnnotationUtils.getAnnotation(frameworkMethod.getMethod(), Repeat.class);
int repeat = (repeatAnnotation != null ? repeatAnnotation.value() : 1);
return new SpringRepeat(next, frameworkMethod.getMethod(), repeat);
}

View File

@@ -20,7 +20,6 @@ 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.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
@@ -28,6 +27,8 @@ 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.*;
/**
* {@code TestExecutionListener} which provides support for marking the
* {@code ApplicationContext} associated with a test as <em>dirty</em> for
@@ -82,9 +83,11 @@ public class DirtiesContextTestExecutionListener extends AbstractTestExecutionLi
final Class<DirtiesContext> annotationType = DirtiesContext.class;
boolean methodDirtiesContext = testMethod.isAnnotationPresent(annotationType);
boolean classDirtiesContext = testClass.isAnnotationPresent(annotationType);
DirtiesContext classDirtiesContextAnnotation = testClass.getAnnotation(annotationType);
DirtiesContext methodDirtiesContextAnnotation = findAnnotation(testMethod, annotationType);
boolean methodDirtiesContext = methodDirtiesContextAnnotation != null;
DirtiesContext classDirtiesContextAnnotation = findAnnotation(testClass, annotationType);
boolean classDirtiesContext = classDirtiesContextAnnotation != null;
ClassMode classMode = classDirtiesContext ? classDirtiesContextAnnotation.classMode() : null;
if (logger.isDebugEnabled()) {
@@ -93,8 +96,8 @@ public class DirtiesContextTestExecutionListener extends AbstractTestExecutionLi
+ methodDirtiesContext + "].");
}
if (methodDirtiesContext || (classDirtiesContext && classMode == ClassMode.AFTER_EACH_TEST_METHOD)) {
HierarchyMode hierarchyMode = methodDirtiesContext ? testMethod.getAnnotation(annotationType).hierarchyMode()
if (methodDirtiesContext || (classMode == ClassMode.AFTER_EACH_TEST_METHOD)) {
HierarchyMode hierarchyMode = methodDirtiesContext ? methodDirtiesContextAnnotation.hierarchyMode()
: classDirtiesContextAnnotation.hierarchyMode();
dirtyContext(testContext, hierarchyMode);
}
@@ -117,12 +120,13 @@ public class DirtiesContextTestExecutionListener extends AbstractTestExecutionLi
final Class<DirtiesContext> annotationType = DirtiesContext.class;
boolean dirtiesContext = testClass.isAnnotationPresent(annotationType);
DirtiesContext dirtiesContextAnnotation = findAnnotation(testClass, annotationType);
boolean dirtiesContext = dirtiesContextAnnotation != null;
if (logger.isDebugEnabled()) {
logger.debug("After test class: context [" + testContext + "], dirtiesContext [" + dirtiesContext + "].");
}
if (dirtiesContext) {
HierarchyMode hierarchyMode = testClass.getAnnotation(annotationType).hierarchyMode();
HierarchyMode hierarchyMode = dirtiesContextAnnotation.hierarchyMode();
dirtyContext(testContext, hierarchyMode);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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.
@@ -17,11 +17,12 @@
package org.springframework.test.context.transaction;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
/**
* <p>
* Test annotation to indicate that the annotated {@code public void}
@@ -37,9 +38,10 @@ import java.lang.annotation.Target;
* @author Sam Brannen
* @since 2.5
* @see org.springframework.transaction.annotation.Transactional
* @see BeforeTransaction
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Retention(RUNTIME)
@Target({ METHOD, ANNOTATION_TYPE })
public @interface AfterTransaction {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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.
@@ -17,11 +17,12 @@
package org.springframework.test.context.transaction;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
/**
* <p>
* Test annotation to indicate that the annotated {@code public void}
@@ -37,9 +38,10 @@ import java.lang.annotation.Target;
* @author Sam Brannen
* @since 2.5
* @see org.springframework.transaction.annotation.Transactional
* @see AfterTransaction
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Retention(RUNTIME)
@Target({ METHOD, ANNOTATION_TYPE })
public @interface BeforeTransaction {
}

View File

@@ -27,14 +27,12 @@ import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
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.AnnotationUtils;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;
@@ -51,6 +49,8 @@ import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import static org.springframework.core.annotation.AnnotationUtils.*;
/**
* {@code TestExecutionListener} that provides support for executing tests
* within transactions by honoring the
@@ -97,16 +97,16 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
private static final Log logger = LogFactory.getLog(TransactionalTestExecutionListener.class);
private static final String DEFAULT_TRANSACTION_MANAGER_NAME = (String) AnnotationUtils.getDefaultValue(
private static final String DEFAULT_TRANSACTION_MANAGER_NAME = (String) getDefaultValue(
TransactionConfiguration.class, "transactionManager");
private static final Boolean DEFAULT_DEFAULT_ROLLBACK = (Boolean) AnnotationUtils.getDefaultValue(
TransactionConfiguration.class, "defaultRollback");
private static final Boolean DEFAULT_DEFAULT_ROLLBACK = (Boolean) getDefaultValue(TransactionConfiguration.class,
"defaultRollback");
protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource();
private final Map<Method, TransactionContext> transactionContextCache =
new ConcurrentHashMap<Method, TransactionContext>(8);
private final Map<Method, TransactionContext> transactionContextCache = new ConcurrentHashMap<Method, TransactionContext>(
8);
private TransactionConfigurationAttributes configurationAttributes;
@@ -153,14 +153,14 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
+ testContext);
}
if (transactionAttribute.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
return;
}
if (transactionAttribute.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
return;
}
tm = getTransactionManager(testContext, transactionAttribute.getQualifier());
}
tm = getTransactionManager(testContext, transactionAttribute.getQualifier());
}
if (tm != null) {
if (tm != null) {
TransactionContext txContext = new TransactionContext(tm, transactionAttribute);
runBeforeTransactionMethods(testContext);
startNewTransaction(testContext, txContext);
@@ -309,7 +309,7 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
* @throws BeansException if an error occurs while retrieving the transaction manager
* @see #getTransactionManager(TestContext)
*/
protected final PlatformTransactionManager getTransactionManager(TestContext testContext, String qualifier) {
protected PlatformTransactionManager getTransactionManager(TestContext testContext, String qualifier) {
// look up by type and qualifier from @Transactional
if (StringUtils.hasText(qualifier)) {
try {
@@ -319,7 +319,8 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory();
return BeanFactoryAnnotationUtils.qualifiedBeanOfType(bf, PlatformTransactionManager.class, qualifier);
} catch (RuntimeException ex) {
}
catch (RuntimeException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Caught exception while retrieving transaction manager for test context " + testContext
+ " and qualifier [" + qualifier + "]", ex);
@@ -341,7 +342,7 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
* @throws BeansException if an error occurs while retrieving the transaction manager
* @see #getTransactionManager(TestContext, String)
*/
protected final PlatformTransactionManager getTransactionManager(TestContext testContext) {
protected PlatformTransactionManager getTransactionManager(TestContext testContext) {
BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory();
String tmName = retrieveConfigurationAttributes(testContext).getTransactionManagerName();
@@ -355,8 +356,8 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
ListableBeanFactory lbf = (ListableBeanFactory) bf;
// look up single bean by type
Map<String, PlatformTransactionManager> txMgrs = BeanFactoryUtils.beansOfTypeIncludingAncestors(
lbf, PlatformTransactionManager.class);
Map<String, PlatformTransactionManager> txMgrs = BeanFactoryUtils.beansOfTypeIncludingAncestors(lbf,
PlatformTransactionManager.class);
if (txMgrs.size() == 1) {
return txMgrs.values().iterator().next();
}
@@ -376,7 +377,8 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
// look up by type and default name from @TransactionConfiguration
return bf.getBean(DEFAULT_TRANSACTION_MANAGER_NAME, PlatformTransactionManager.class);
} catch (BeansException ex) {
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Caught exception while retrieving transaction manager for test context " + testContext, ex);
}
@@ -408,7 +410,7 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
*/
protected final boolean isRollback(TestContext testContext) throws Exception {
boolean rollback = isDefaultRollback(testContext);
Rollback rollbackAnnotation = testContext.getTestMethod().getAnnotation(Rollback.class);
Rollback rollbackAnnotation = findAnnotation(testContext.getTestMethod(), Rollback.class);
if (rollbackAnnotation != null) {
boolean rollbackOverride = rollbackAnnotation.value();
if (logger.isDebugEnabled()) {
@@ -429,17 +431,17 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
/**
* Gets all superclasses of the supplied {@link Class class}, including the
* class itself. The ordering of the returned list will begin with the
* supplied class and continue up the class hierarchy.
* supplied class and continue up the class hierarchy, excluding {@link Object}.
* <p>Note: This code has been borrowed from
* {@link org.junit.internal.runners.TestClass#getSuperClasses(Class)} and
* adapted.
* @param clazz the class for which to retrieve the superclasses.
* @return all superclasses of the supplied class.
* @param clazz the class for which to retrieve the superclasses
* @return all superclasses of the supplied class, excluding {@code Object}
*/
private List<Class<?>> getSuperClasses(Class<?> clazz) {
ArrayList<Class<?>> results = new ArrayList<Class<?>>();
List<Class<?>> results = new ArrayList<Class<?>>();
Class<?> current = clazz;
while (current != null) {
while (current != null && !current.equals(Object.class)) {
results.add(current);
current = current.getSuperclass();
}
@@ -459,12 +461,11 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
*/
private List<Method> getAnnotatedMethods(Class<?> clazz, Class<? extends Annotation> annotationType) {
List<Method> results = new ArrayList<Method>();
for (Class<?> eachClass : getSuperClasses(clazz)) {
Method[] methods = eachClass.getDeclaredMethods();
for (Method eachMethod : methods) {
Annotation annotation = eachMethod.getAnnotation(annotationType);
if (annotation != null && !isShadowed(eachMethod, results)) {
results.add(eachMethod);
for (Class<?> current : getSuperClasses(clazz)) {
for (Method method : current.getDeclaredMethods()) {
Annotation annotation = getAnnotation(method, annotationType);
if (annotation != null && !isShadowed(method, results)) {
results.add(method);
}
}
}
@@ -472,8 +473,8 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
}
/**
* Determines if the supplied {@link Method method} is <em>shadowed</em>
* by a method in supplied {@link List list} of previous methods.
* Determine if the supplied {@link Method method} is <em>shadowed</em> by
* a method in the supplied {@link List list} of previous methods.
* <p>Note: This code has been borrowed from
* {@link org.junit.internal.runners.TestClass#isShadowed(Method, List)}.
* @param method the method to check for shadowing
@@ -491,8 +492,8 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
}
/**
* Determines if the supplied {@link Method current method} is
* <em>shadowed</em> by a {@link Method previous method}.
* Determine if the supplied {@link Method current method} is <em>shadowed</em>
* by a {@link Method previous method}.
* <p>Note: This code has been borrowed from
* {@link org.junit.internal.runners.TestClass#isShadowed(Method, Method)}.
* @param current the current method
@@ -528,7 +529,7 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
private TransactionConfigurationAttributes retrieveConfigurationAttributes(TestContext testContext) {
if (this.configurationAttributes == null) {
Class<?> clazz = testContext.getTestClass();
TransactionConfiguration config = clazz.getAnnotation(TransactionConfiguration.class);
TransactionConfiguration config = findAnnotation(clazz, TransactionConfiguration.class);
if (logger.isDebugEnabled()) {
logger.debug("Retrieved @TransactionConfiguration [" + config + "] for test class [" + clazz + "]");
}

View File

@@ -0,0 +1,307 @@
/*
* Copyright 2002-2013 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
*
* http://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.transaction;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.test.context.TestContext;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.SimpleTransactionStatus;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* Unit tests for {@link TransactionalTestExecutionListener}.
*
* @author Sam Brannen
* @since 4.0
*/
public class TransactionalTestExecutionListenerTests {
private final PlatformTransactionManager tm = mock(PlatformTransactionManager.class);
private final TransactionalTestExecutionListener listener = new TransactionalTestExecutionListener() {
protected PlatformTransactionManager getTransactionManager(TestContext testContext, String qualifier) {
return tm;
}
};
private final TestContext testContext = mock(TestContext.class);
private void assertBeforeTestMethod(Class<? extends Invocable> clazz) throws Exception {
assertBeforeTestMethodWithTransactionalTestMethod(clazz);
assertBeforeTestMethodWithNonTransactionalTestMethod(clazz);
}
private void assertBeforeTestMethodWithTransactionalTestMethod(Class<? extends Invocable> clazz) throws Exception {
Mockito.<Class<?>> when(testContext.getTestClass()).thenReturn(clazz);
Invocable instance = clazz.newInstance();
when(testContext.getTestInstance()).thenReturn(instance);
when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("transactionalTest"));
assertFalse(instance.invoked);
listener.beforeTestMethod(testContext);
assertTrue(instance.invoked);
}
private void assertBeforeTestMethodWithNonTransactionalTestMethod(Class<? extends Invocable> clazz)
throws Exception {
Mockito.<Class<?>> when(testContext.getTestClass()).thenReturn(clazz);
Invocable instance = clazz.newInstance();
when(testContext.getTestInstance()).thenReturn(instance);
when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("nonTransactionalTest"));
assertFalse(instance.invoked);
listener.beforeTestMethod(testContext);
assertFalse(instance.invoked);
}
private void assertAfterTestMethod(Class<? extends Invocable> clazz) throws Exception {
assertAfterTestMethodWithTransactionalTestMethod(clazz);
assertAfterTestMethodWithNonTransactionalTestMethod(clazz);
}
private void assertAfterTestMethodWithTransactionalTestMethod(Class<? extends Invocable> clazz) throws Exception {
Mockito.<Class<?>> when(testContext.getTestClass()).thenReturn(clazz);
Invocable instance = clazz.newInstance();
when(testContext.getTestInstance()).thenReturn(instance);
when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("transactionalTest"));
when(tm.getTransaction(Mockito.any(TransactionDefinition.class))).thenReturn(new SimpleTransactionStatus());
assertFalse(instance.invoked);
listener.beforeTestMethod(testContext);
listener.afterTestMethod(testContext);
assertTrue(instance.invoked);
}
private void assertAfterTestMethodWithNonTransactionalTestMethod(Class<? extends Invocable> clazz) throws Exception {
Mockito.<Class<?>> when(testContext.getTestClass()).thenReturn(clazz);
Invocable instance = clazz.newInstance();
when(testContext.getTestInstance()).thenReturn(instance);
when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("nonTransactionalTest"));
assertFalse(instance.invoked);
listener.beforeTestMethod(testContext);
listener.afterTestMethod(testContext);
assertFalse(instance.invoked);
}
@Test
public void beforeTestMethodWithTransactionalDeclaredOnClassLocally() throws Exception {
assertBeforeTestMethodWithTransactionalTestMethod(TransactionalDeclaredOnClassLocallyTestCase.class);
}
@Test
public void beforeTestMethodWithTransactionalDeclaredOnClassViaMetaAnnotation() throws Exception {
assertBeforeTestMethodWithTransactionalTestMethod(TransactionalDeclaredOnClassViaMetaAnnotationTestCase.class);
}
@Test
public void beforeTestMethodWithTransactionalDeclaredOnMethodLocally() throws Exception {
assertBeforeTestMethod(TransactionalDeclaredOnMethodLocallyTestCase.class);
}
@Test
public void beforeTestMethodWithTransactionalDeclaredOnMethodViaMetaAnnotation() throws Exception {
assertBeforeTestMethod(TransactionalDeclaredOnMethodViaMetaAnnotationTestCase.class);
}
@Test
public void beforeTestMethodWithBeforeTransactionDeclaredLocally() throws Exception {
assertBeforeTestMethod(BeforeTransactionDeclaredLocallyTestCase.class);
}
@Test
public void beforeTestMethodWithBeforeTransactionDeclaredViaMetaAnnotation() throws Exception {
assertBeforeTestMethod(BeforeTransactionDeclaredViaMetaAnnotationTestCase.class);
}
@Test
public void afterTestMethodWithAfterTransactionDeclaredLocally() throws Exception {
assertAfterTestMethod(AfterTransactionDeclaredLocallyTestCase.class);
}
@Test
public void afterTestMethodWithAfterTransactionDeclaredViaMetaAnnotation() throws Exception {
assertAfterTestMethod(AfterTransactionDeclaredViaMetaAnnotationTestCase.class);
}
// -------------------------------------------------------------------------
@Transactional
@Retention(RetentionPolicy.RUNTIME)
private static @interface MetaTransactional {
}
@BeforeTransaction
@Retention(RetentionPolicy.RUNTIME)
private static @interface MetaBeforeTransaction {
}
@AfterTransaction
@Retention(RetentionPolicy.RUNTIME)
private static @interface MetaAfterTransaction {
}
private static abstract class Invocable {
boolean invoked = false;
}
@Transactional
static class TransactionalDeclaredOnClassLocallyTestCase extends Invocable {
@BeforeTransaction
public void beforeTransaction() {
invoked = true;
}
public void transactionalTest() {
/* no-op */
}
}
static class TransactionalDeclaredOnMethodLocallyTestCase extends Invocable {
@BeforeTransaction
public void beforeTransaction() {
invoked = true;
}
@Transactional
public void transactionalTest() {
/* no-op */
}
public void nonTransactionalTest() {
/* no-op */
}
}
@MetaTransactional
static class TransactionalDeclaredOnClassViaMetaAnnotationTestCase extends Invocable {
@BeforeTransaction
public void beforeTransaction() {
invoked = true;
}
public void transactionalTest() {
/* no-op */
}
public void nonTransactionalTest() {
/* no-op */
}
}
static class TransactionalDeclaredOnMethodViaMetaAnnotationTestCase extends Invocable {
@BeforeTransaction
public void beforeTransaction() {
invoked = true;
}
@MetaTransactional
public void transactionalTest() {
/* no-op */
}
public void nonTransactionalTest() {
/* no-op */
}
}
static class BeforeTransactionDeclaredLocallyTestCase extends Invocable {
@BeforeTransaction
public void beforeTransaction() {
invoked = true;
}
@Transactional
public void transactionalTest() {
/* no-op */
}
public void nonTransactionalTest() {
/* no-op */
}
}
static class BeforeTransactionDeclaredViaMetaAnnotationTestCase extends Invocable {
@MetaBeforeTransaction
public void beforeTransaction() {
invoked = true;
}
@Transactional
public void transactionalTest() {
/* no-op */
}
public void nonTransactionalTest() {
/* no-op */
}
}
static class AfterTransactionDeclaredLocallyTestCase extends Invocable {
@AfterTransaction
public void afterTransaction() {
invoked = true;
}
@Transactional
public void transactionalTest() {
/* no-op */
}
public void nonTransactionalTest() {
/* no-op */
}
}
static class AfterTransactionDeclaredViaMetaAnnotationTestCase extends Invocable {
@MetaAfterTransaction
public void afterTransaction() {
invoked = true;
}
@Transactional
public void transactionalTest() {
/* no-op */
}
public void nonTransactionalTest() {
/* no-op */
}
}
}