diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index 56350dd70d0..e86ad1c6b52 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -285,9 +285,8 @@ public abstract class AnnotationUtils { *

The standard {@link Class} API does not provide a mechanism for determining which class * in an inheritance hierarchy actually declares an {@link Annotation}, so we need to handle * this explicitly. - * @param annotationType the Class object corresponding to the annotation type - * @param clazz the Class object corresponding to the class on which to check for the annotation, - * or {@code null} + * @param annotationType the annotation class to look for, both locally and as a meta-annotation + * @param clazz the class on which to check for the annotation, or {@code null} * @return the first {@link Class} in the inheritance hierarchy of the specified {@code clazz} * which declares an annotation for the specified {@code annotationType}, or {@code null} * if not found @@ -301,8 +300,24 @@ public abstract class AnnotationUtils { if (clazz == null || clazz.equals(Object.class)) { return null; } - return (isAnnotationDeclaredLocally(annotationType, clazz)) ? clazz : findAnnotationDeclaringClass( - annotationType, clazz.getSuperclass()); + + // Declared locally? + if (isAnnotationDeclaredLocally(annotationType, clazz)) { + return clazz; + } + + // Declared on a stereotype annotation (i.e., as a meta-annotation)? + if (!Annotation.class.isAssignableFrom(clazz)) { + for (Annotation stereotype : clazz.getAnnotations()) { + Class declaringClass = findAnnotationDeclaringClass(annotationType, stereotype.annotationType()); + if (declaringClass != null) { + return declaringClass; + } + } + } + + // Declared on a superclass? + return findAnnotationDeclaringClass(annotationType, clazz.getSuperclass()); } /** diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java index 1163704d861..e5ec3f1fc1c 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java @@ -96,6 +96,18 @@ public class AnnotationUtilsTests { // assertNotNull(o); // } + @Test + public void findAnnotationPrefersInteracesOverLocalMetaAnnotations() { + Component component = AnnotationUtils.findAnnotation( + ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, Component.class); + + // By inspecting ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface, one + // might expect that "meta2" should be found; however, with the current + // implementation "meta1" will be found. + assertNotNull(component); + assertEquals("meta1", component.value()); + } + @Test public void testFindAnnotationDeclaringClass() throws Exception { // no class-level annotation @@ -133,7 +145,7 @@ public class AnnotationUtilsTests { assertEquals(InheritedAnnotationInterface.class, findAnnotationDeclaringClassForTypes(transactionalCandidateList, InheritedAnnotationInterface.class)); assertNull(findAnnotationDeclaringClassForTypes(transactionalCandidateList, - SubInheritedAnnotationInterface.class)); + SubInheritedAnnotationInterface.class)); assertEquals(InheritedAnnotationClass.class, findAnnotationDeclaringClassForTypes(transactionalCandidateList, InheritedAnnotationClass.class)); assertEquals(InheritedAnnotationClass.class, @@ -288,19 +300,23 @@ public class AnnotationUtilsTests { @Component(value = "meta1") + @Order @Retention(RetentionPolicy.RUNTIME) @interface Meta1 { } @Component(value = "meta2") + @Transactional @Retention(RetentionPolicy.RUNTIME) @interface Meta2 { } @Meta1 - @Component(value = "local") + static interface InterfaceWithMetaAnnotation { + } + @Meta2 - static class HasLocalAndMetaComponentAnnotation { + static class ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface implements InterfaceWithMetaAnnotation { } public static interface AnnotatedInterface { diff --git a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java index 922e34c6295..1e54f68b237 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java @@ -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 profile values. * @@ -63,7 +65,7 @@ public abstract class ProfileValueUtils { Assert.notNull(testClass, "testClass must not be null"); Class 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); } diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java b/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java index 4c3dcd3a91e..7b7d04816ff 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java @@ -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. - *

- * Note that the scope of execution to be repeated includes execution of the + * + *

Note that the scope of execution to be repeated includes execution of the * test method itself as well as any set up or tear down 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; diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Timed.java b/spring-test/src/main/java/org/springframework/test/annotation/Timed.java index c71d8477b92..fc7c6e0bc09 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/Timed.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/Timed.java @@ -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.*; + /** - *

* Test-specific annotation to indicate that a test method has to finish * execution in a {@link #millis() specified time period}. - *

- *

- * If the text execution takes longer than the specified time period, then the - * test is to be considered failed. - *

- *

- * Note that the time period includes execution of the test method itself, any - * {@link Repeat repetitions} of the test, and any set up or + * + *

If the text execution takes longer than the specified time period, then + * the test is to be considered failed. + * + *

Note that the time period includes execution of the test method itself, + * any {@link Repeat repetitions} of the test, and any set up or * tear down of the test fixture. - *

* * @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 { /** diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java b/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java index 031f12c6d30..ef60606b0ab 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java @@ -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 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> resolveContextHierarchyAttributes(Class testClass) { Assert.notNull(testClass, "Class must not be null"); final Class contextConfigType = ContextConfiguration.class; final Class contextHierarchyType = ContextHierarchy.class; - final List> annotationTypes = Arrays.asList(contextConfigType, contextHierarchyType); - final List> hierarchyAttributes = new ArrayList>(); - 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 configAttributesList = new ArrayList(); 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 attributesList = new ArrayList(); Class annotationType = ContextConfiguration.class; - Class declaringClass = findAnnotationDeclaringClass(annotationType, testClass); - Assert.notNull(declaringClass, String.format( + + AnnotationDescriptor 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 annotationType = ActiveProfiles.class; - Class declaringClass = findAnnotationDeclaringClass(annotationType, testClass); - if (declaringClass == null && logger.isDebugEnabled()) { + AnnotationDescriptor 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 activeProfiles = new HashSet(); - 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> hierarchyMap = buildContextHierarchyMap(testClass); MergedContextConfiguration parentConfig = null; @@ -729,28 +755,32 @@ abstract class ContextLoaderUtils { CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parentConfig) { Class 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 webMergedConfigClass = (Class) ClassUtils.forName( - WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME, ContextLoaderUtils.class.getClassLoader()); + String resourceBasePath = (String) AnnotationUtils.getValue(annotation); - Constructor constructor = ClassUtils.getConstructorIfAvailable( - webMergedConfigClass, Class.class, String[].class, Class[].class, Set.class, String[].class, - String.class, ContextLoader.class, CacheAwareContextLoaderDelegate.class, - MergedContextConfiguration.class); + try { + Class webMergedConfigClass = (Class) 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 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); + } } } } diff --git a/spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java b/spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java index 73ba7dd4a9c..22d6580a3df 100644 --- a/spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java @@ -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); } /** diff --git a/spring-test/src/main/java/org/springframework/test/context/MetaAnnotationUtils.java b/spring-test/src/main/java/org/springframework/test/context/MetaAnnotationUtils.java new file mode 100644 index 00000000000..b891c0f2d9c --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/MetaAnnotationUtils.java @@ -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 AnnotationDescriptor findAnnotationDescriptor(Class clazz, + Class 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(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(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... 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 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 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 declared + * as well as the actual {@linkplain #getAnnotation() annotation} instance. + * + *

+ * 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 declaring class is not directly + * annotated with the annotation but rather indirectly via the stereotype. + * + *

+ * Given the following example, if we are searching for the {@code @Transactional} + * annotation on the {@code TransactionalTests} class, then the + * properties of the {@code AnnotationDescriptor} would be as follows. + * + *

    + *
  • declaringClass: {@code TransactionalTests} class object
  • + *
  • stereotype: {@code null}
  • + *
  • annotation: instance of the {@code Transactional} annotation
  • + *
+ * + *
+	 * @Transactional
+	 * @ContextConfiguration({"/test-datasource.xml", "/repository-config.xml"})
+	 * public class TransactionalTests { }
+	 * 
+ * + *

+ * Given the following example, if we are searching for the {@code @Transactional} + * annotation on the {@code UserRepositoryTests} class, then the + * properties of the {@code AnnotationDescriptor} would be as follows. + * + *

    + *
  • declaringClass: {@code UserRepositoryTests} class object
  • + *
  • stereotype: instance of the {@code RepositoryTests} annotation
  • + *
  • annotation: instance of the {@code Transactional} annotation
  • + *
+ * + *
+	 * @Transactional
+	 * @ContextConfiguration({"/test-datasource.xml", "/repository-config.xml"})
+	 * @Retention(RetentionPolicy.RUNTIME)
+	 * public @interface RepositoryTests { }
+	 *
+	 * @RepositoryTests
+	 * public class UserRepositoryTests { }
+	 * 
+ * + * @author Sam Brannen + * @since 4.0 + */ + public static class AnnotationDescriptor { + + 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 getAnnotationType() { + return this.annotation.annotationType(); + } + + public Annotation getStereotype() { + return this.stereotype; + } + + public Class 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 { + + 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"); + } + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java index 02649aaf9a7..22c6919bb35 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java @@ -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.*; + /** *

* {@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 annotationType = TestExecutionListeners.class; List> classesList = new ArrayList>(); - Class declaringClass = AnnotationUtils.findAnnotationDeclaringClass(annotationType, clazz); + + AnnotationDescriptor 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.> asList(listenerClasses)); } - declaringClass = (testExecutionListeners.inheritListeners() ? AnnotationUtils.findAnnotationDeclaringClass( - annotationType, declaringClass.getSuperclass()) : null); + + descriptor = (testExecutionListeners.inheritListeners() ? findAnnotationDescriptor( + rootDeclaringClass.getSuperclass(), annotationType) : null); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java index cb40a3273da..79c577b764f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java @@ -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); } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java index 168ac59bed6..50bb5aa323c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DirtiesContextTestExecutionListener.java @@ -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 dirty for @@ -82,9 +83,11 @@ public class DirtiesContextTestExecutionListener extends AbstractTestExecutionLi final Class 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 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); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java b/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java index bb25f1f8e64..c05b12c6b07 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java @@ -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.*; + /** *

* 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 { } diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java b/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java index d36250099dd..a39cb19007e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java @@ -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.*; + /** *

* 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 { } diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java index 4846f0955e9..428d178f8ae 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java @@ -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 transactionContextCache = - new ConcurrentHashMap(8); + private final Map transactionContextCache = new ConcurrentHashMap( + 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 txMgrs = BeanFactoryUtils.beansOfTypeIncludingAncestors( - lbf, PlatformTransactionManager.class); + Map 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}. *

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> getSuperClasses(Class clazz) { - ArrayList> results = new ArrayList>(); + List> results = new ArrayList>(); 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 getAnnotatedMethods(Class clazz, Class annotationType) { List results = new ArrayList(); - 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 shadowed - * by a method in supplied {@link List list} of previous methods. + * Determine if the supplied {@link Method method} is shadowed by + * a method in the supplied {@link List list} of previous methods. *

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 - * shadowed by a {@link Method previous method}. + * Determine if the supplied {@link Method current method} is shadowed + * by a {@link Method previous method}. *

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 + "]"); } diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListenerTests.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListenerTests.java new file mode 100644 index 00000000000..83568850cb5 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListenerTests.java @@ -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 clazz) throws Exception { + assertBeforeTestMethodWithTransactionalTestMethod(clazz); + assertBeforeTestMethodWithNonTransactionalTestMethod(clazz); + } + + private void assertBeforeTestMethodWithTransactionalTestMethod(Class clazz) throws Exception { + Mockito.> 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 clazz) + throws Exception { + Mockito.> 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 clazz) throws Exception { + assertAfterTestMethodWithTransactionalTestMethod(clazz); + assertAfterTestMethodWithNonTransactionalTestMethod(clazz); + } + + private void assertAfterTestMethodWithTransactionalTestMethod(Class clazz) throws Exception { + Mockito.> 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 clazz) throws Exception { + Mockito.> 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 */ + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueUtilsTests.java b/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueUtilsTests.java index bb715753aff..4296fd077da 100644 --- a/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueUtilsTests.java @@ -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. @@ -16,14 +16,15 @@ package org.springframework.test.annotation; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; import org.junit.BeforeClass; import org.junit.Test; +import static org.junit.Assert.*; + /** * Unit tests for {@link ProfileValueUtils}. * @@ -88,8 +89,12 @@ public class ProfileValueUtilsTests { assertClassIsEnabled(NonAnnotated.class); assertClassIsEnabled(EnabledAnnotatedSingleValue.class); assertClassIsEnabled(EnabledAnnotatedMultiValue.class); + assertClassIsEnabled(MetaEnabledClass.class); + assertClassIsEnabled(MetaEnabledWithCustomProfileValueSourceClass.class); assertClassIsDisabled(DisabledAnnotatedSingleValue.class); assertClassIsDisabled(DisabledAnnotatedMultiValue.class); + assertClassIsDisabled(MetaDisabledClass.class); + assertClassIsDisabled(MetaDisabledWithCustomProfileValueSourceClass.class); } @Test @@ -100,6 +105,10 @@ public class ProfileValueUtilsTests { assertMethodIsEnabled(ENABLED_ANNOTATED_METHOD, EnabledAnnotatedSingleValue.class); assertMethodIsDisabled(DISABLED_ANNOTATED_METHOD, EnabledAnnotatedSingleValue.class); + assertMethodIsEnabled(NON_ANNOTATED_METHOD, MetaEnabledAnnotatedSingleValue.class); + assertMethodIsEnabled(ENABLED_ANNOTATED_METHOD, MetaEnabledAnnotatedSingleValue.class); + assertMethodIsDisabled(DISABLED_ANNOTATED_METHOD, MetaEnabledAnnotatedSingleValue.class); + assertMethodIsEnabled(NON_ANNOTATED_METHOD, EnabledAnnotatedMultiValue.class); assertMethodIsEnabled(ENABLED_ANNOTATED_METHOD, EnabledAnnotatedMultiValue.class); assertMethodIsDisabled(DISABLED_ANNOTATED_METHOD, EnabledAnnotatedMultiValue.class); @@ -108,6 +117,10 @@ public class ProfileValueUtilsTests { assertMethodIsDisabled(ENABLED_ANNOTATED_METHOD, DisabledAnnotatedSingleValue.class); assertMethodIsDisabled(DISABLED_ANNOTATED_METHOD, DisabledAnnotatedSingleValue.class); + assertMethodIsDisabled(NON_ANNOTATED_METHOD, MetaDisabledAnnotatedSingleValue.class); + assertMethodIsDisabled(ENABLED_ANNOTATED_METHOD, MetaDisabledAnnotatedSingleValue.class); + assertMethodIsDisabled(DISABLED_ANNOTATED_METHOD, MetaDisabledAnnotatedSingleValue.class); + assertMethodIsDisabled(NON_ANNOTATED_METHOD, DisabledAnnotatedMultiValue.class); assertMethodIsDisabled(ENABLED_ANNOTATED_METHOD, DisabledAnnotatedMultiValue.class); assertMethodIsDisabled(DISABLED_ANNOTATED_METHOD, DisabledAnnotatedMultiValue.class); @@ -211,4 +224,82 @@ public class ProfileValueUtilsTests { } } + @IfProfileValue(name = NAME, value = VALUE) + @Retention(RetentionPolicy.RUNTIME) + private static @interface MetaEnabled { + } + + @IfProfileValue(name = NAME, value = VALUE + "X") + @Retention(RetentionPolicy.RUNTIME) + private static @interface MetaDisabled { + } + + @MetaEnabled + private static class MetaEnabledClass { + } + + @MetaDisabled + private static class MetaDisabledClass { + } + + @SuppressWarnings("unused") + @MetaEnabled + private static class MetaEnabledAnnotatedSingleValue { + + public void nonAnnotatedMethod() { + } + + @MetaEnabled + public void enabledAnnotatedMethod() { + } + + @MetaDisabled + public void disabledAnnotatedMethod() { + } + } + + @SuppressWarnings("unused") + @MetaDisabled + private static class MetaDisabledAnnotatedSingleValue { + + public void nonAnnotatedMethod() { + } + + @MetaEnabled + public void enabledAnnotatedMethod() { + } + + @MetaDisabled + public void disabledAnnotatedMethod() { + } + } + + public static class HardCodedProfileValueSource implements ProfileValueSource { + + @Override + public String get(final String key) { + return (key.equals(NAME) ? "42" : null); + } + } + + @ProfileValueSourceConfiguration(HardCodedProfileValueSource.class) + @IfProfileValue(name = NAME, value = "42") + @Retention(RetentionPolicy.RUNTIME) + private static @interface MetaEnabledWithCustomProfileValueSource { + } + + @ProfileValueSourceConfiguration(HardCodedProfileValueSource.class) + @IfProfileValue(name = NAME, value = "13") + @Retention(RetentionPolicy.RUNTIME) + private static @interface MetaDisabledWithCustomProfileValueSource { + } + + @MetaEnabledWithCustomProfileValueSource + private static class MetaEnabledWithCustomProfileValueSourceClass { + } + + @MetaDisabledWithCustomProfileValueSource + private static class MetaDisabledWithCustomProfileValueSourceClass { + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/AbstractContextLoaderUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/AbstractContextLoaderUtilsTests.java index 6942771cfe1..e2a6fe305ec 100644 --- a/spring-test/src/test/java/org/springframework/test/context/AbstractContextLoaderUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/AbstractContextLoaderUtilsTests.java @@ -16,6 +16,10 @@ package org.springframework.test.context; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.Collections; import java.util.Set; @@ -40,6 +44,16 @@ abstract class AbstractContextLoaderUtilsTests { Collections.>> emptySet(); + void assertAttributes(ContextConfigurationAttributes attributes, Class expectedDeclaringClass, + String[] expectedLocations, Class[] expectedClasses, + Class expectedContextLoaderClass, boolean expectedInheritLocations) { + assertEquals(expectedDeclaringClass, attributes.getDeclaringClass()); + assertArrayEquals(expectedLocations, attributes.getLocations()); + assertArrayEquals(expectedClasses, attributes.getClasses()); + assertEquals(expectedInheritLocations, attributes.isInheritLocations()); + assertEquals(expectedContextLoaderClass, attributes.getContextLoaderClass()); + } + void assertMergedConfig(MergedContextConfiguration mergedConfig, Class expectedTestClass, String[] expectedLocations, Class[] expectedClasses, Class expectedContextLoaderClass) { @@ -61,7 +75,13 @@ abstract class AbstractContextLoaderUtilsTests { assertNotNull(mergedConfig.getClasses()); assertArrayEquals(expectedClasses, mergedConfig.getClasses()); assertNotNull(mergedConfig.getActiveProfiles()); - assertEquals(expectedContextLoaderClass, mergedConfig.getContextLoader().getClass()); + System.err.println(expectedContextLoaderClass); + if (expectedContextLoaderClass == null) { + assertNull(mergedConfig.getContextLoader()); + } + else { + assertEquals(expectedContextLoaderClass, mergedConfig.getContextLoader().getClass()); + } assertNotNull(mergedConfig.getContextInitializerClasses()); assertEquals(expectedInitializerClasses, mergedConfig.getContextInitializerClasses()); } @@ -83,6 +103,28 @@ abstract class AbstractContextLoaderUtilsTests { static class BarConfig { } + @ContextConfiguration("/foo.xml") + @ActiveProfiles(profiles = "foo") + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public static @interface MetaLocationsFooConfig { + } + + @ContextConfiguration("/bar.xml") + @ActiveProfiles(profiles = "bar") + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public static @interface MetaLocationsBarConfig { + } + + @MetaLocationsFooConfig + static class MetaLocationsFoo { + } + + @MetaLocationsBarConfig + static class MetaLocationsBar extends MetaLocationsFoo { + } + @ContextConfiguration(locations = "/foo.xml", inheritLocations = false) @ActiveProfiles(profiles = "foo") static class LocationsFoo { diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsActiveProfilesTests.java b/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsActiveProfilesTests.java index 90bfcf00765..d1c49ad2783 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsActiveProfilesTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsActiveProfilesTests.java @@ -16,6 +16,10 @@ package org.springframework.test.context; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.Arrays; import java.util.List; @@ -107,6 +111,44 @@ public class ContextLoaderUtilsActiveProfilesTests extends AbstractContextLoader assertTrue(list.contains("cat")); } + /** + * @since 4.0 + */ + @Test + public void resolveActiveProfilesWithMetaAnnotation() { + String[] profiles = resolveActiveProfiles(MetaLocationsFoo.class); + assertNotNull(profiles); + assertArrayEquals(new String[] { "foo" }, profiles); + } + + /** + * @since 4.0 + */ + @Test + public void resolveActiveProfilesWithLocalAndInheritedMetaAnnotations() { + String[] profiles = resolveActiveProfiles(MetaLocationsBar.class); + assertNotNull(profiles); + assertEquals(2, profiles.length); + + List list = Arrays.asList(profiles); + assertTrue(list.contains("foo")); + assertTrue(list.contains("bar")); + } + + /** + * @since 4.0 + */ + @Test + public void resolveActiveProfilesWithOverriddenMetaAnnotation() { + String[] profiles = resolveActiveProfiles(MetaAnimals.class); + assertNotNull(profiles); + assertEquals(2, profiles.length); + + List list = Arrays.asList(profiles); + assertTrue(list.contains("dog")); + assertTrue(list.contains("cat")); + } + /** * @since 4.0 */ @@ -208,6 +250,16 @@ public class ContextLoaderUtilsActiveProfilesTests extends AbstractContextLoader private static class Animals extends LocationsBar { } + @ActiveProfiles(profiles = { "dog", "cat" }, inheritProfiles = false) + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + private static @interface MetaAnimalsConfig { + } + + @MetaAnimalsConfig + private static class MetaAnimals extends MetaLocationsBar { + } + private static class InheritedLocationsFoo extends LocationsFoo { } diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsConfigurationAttributesTests.java b/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsConfigurationAttributesTests.java index 30945811558..471243eb42a 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsConfigurationAttributesTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsConfigurationAttributesTests.java @@ -32,16 +32,6 @@ import static org.springframework.test.context.ContextLoaderUtils.*; */ public class ContextLoaderUtilsConfigurationAttributesTests extends AbstractContextLoaderUtilsTests { - private void assertAttributes(ContextConfigurationAttributes attributes, Class expectedDeclaringClass, - String[] expectedLocations, Class[] expectedClasses, - Class expectedContextLoaderClass, boolean expectedInheritLocations) { - assertEquals(expectedDeclaringClass, attributes.getDeclaringClass()); - assertArrayEquals(expectedLocations, attributes.getLocations()); - assertArrayEquals(expectedClasses, attributes.getClasses()); - assertEquals(expectedInheritLocations, attributes.isInheritLocations()); - assertEquals(expectedContextLoaderClass, attributes.getContextLoaderClass()); - } - private void assertLocationsFooAttributes(ContextConfigurationAttributes attributes) { assertAttributes(attributes, LocationsFoo.class, new String[] { "/foo.xml" }, EMPTY_CLASS_ARRAY, ContextLoader.class, false); @@ -84,6 +74,26 @@ public class ContextLoaderUtilsConfigurationAttributesTests extends AbstractCont assertLocationsFooAttributes(attributesList.get(0)); } + @Test + public void resolveConfigAttributesWithMetaAnnotationAndLocations() { + List attributesList = resolveContextConfigurationAttributes(MetaLocationsFoo.class); + assertNotNull(attributesList); + assertEquals(1, attributesList.size()); + assertAttributes(attributesList.get(0), MetaLocationsFooConfig.class, new String[] { "/foo.xml" }, + EMPTY_CLASS_ARRAY, ContextLoader.class, true); + } + + @Test + public void resolveConfigAttributesWithMetaAnnotationAndLocationsInClassHierarchy() { + List attributesList = resolveContextConfigurationAttributes(MetaLocationsBar.class); + assertNotNull(attributesList); + assertEquals(2, attributesList.size()); + assertAttributes(attributesList.get(0), MetaLocationsBarConfig.class, new String[] { "/bar.xml" }, + EMPTY_CLASS_ARRAY, ContextLoader.class, true); + assertAttributes(attributesList.get(1), MetaLocationsFooConfig.class, new String[] { "/foo.xml" }, + EMPTY_CLASS_ARRAY, ContextLoader.class, true); + } + @Test public void resolveConfigAttributesWithLocalAnnotationAndClasses() { List attributesList = resolveContextConfigurationAttributes(ClassesFoo.class); diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsContextHierarchyTests.java index fc512e2dd39..2b839bbbca4 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsContextHierarchyTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsContextHierarchyTests.java @@ -16,6 +16,8 @@ package org.springframework.test.context; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -33,7 +35,7 @@ import static org.springframework.test.context.ContextLoaderUtils.*; * Unit tests for {@link ContextLoaderUtils} involving context hierarchies. * * @author Sam Brannen - * @since 3.1 + * @since 3.2.2 */ public class ContextLoaderUtilsContextHierarchyTests extends AbstractContextLoaderUtilsTests { @@ -48,6 +50,11 @@ public class ContextLoaderUtilsContextHierarchyTests extends AbstractContextLoad resolveContextHierarchyAttributes(SingleTestClassWithContextConfigurationAndContextHierarchy.class); } + @Test(expected = IllegalStateException.class) + public void resolveContextHierarchyAttributesForSingleTestClassWithContextConfigurationAndContextHierarchyOnSingleMetaAnnotation() { + resolveContextHierarchyAttributes(SingleTestClassWithContextConfigurationAndContextHierarchyOnSingleMetaAnnotation.class); + } + @Test public void resolveContextHierarchyAttributesForSingleTestClassWithImplicitSingleLevelContextHierarchy() { List> hierarchyAttributes = resolveContextHierarchyAttributes(BareAnnotations.class); @@ -67,12 +74,34 @@ public class ContextLoaderUtilsContextHierarchyTests extends AbstractContextLoad } @Test - public void resolveContextHierarchyAttributesForSingleTestClassWithTripleLevelContextHierarchy() { - List> hierarchyAttributes = resolveContextHierarchyAttributes(SingleTestClassWithTripleLevelContextHierarchy.class); + public void resolveContextHierarchyAttributesForSingleTestClassWithSingleLevelContextHierarchyFromMetaAnnotation() { + List> hierarchyAttributes = resolveContextHierarchyAttributes(SingleTestClassWithSingleLevelContextHierarchyFromMetaAnnotation.class); assertEquals(1, hierarchyAttributes.size()); + List configAttributesList = hierarchyAttributes.get(0); + assertNotNull(configAttributesList); + assertEquals(1, configAttributesList.size()); + debugConfigAttributes(configAttributesList); + assertAttributes(configAttributesList.get(0), ContextHierarchyA.class, new String[] { "A.xml" }, + EMPTY_CLASS_ARRAY, ContextLoader.class, true); + } + + @Test + public void resolveContextHierarchyAttributesForSingleTestClassWithTripleLevelContextHierarchy() { + Class testClass = SingleTestClassWithTripleLevelContextHierarchy.class; + List> hierarchyAttributes = resolveContextHierarchyAttributes(testClass); + assertEquals(1, hierarchyAttributes.size()); + + List configAttributesList = hierarchyAttributes.get(0); + assertNotNull(configAttributesList); assertEquals(3, configAttributesList.size()); debugConfigAttributes(configAttributesList); + assertAttributes(configAttributesList.get(0), testClass, new String[] { "A.xml" }, EMPTY_CLASS_ARRAY, + ContextLoader.class, true); + assertAttributes(configAttributesList.get(1), testClass, new String[] { "B.xml" }, + EMPTY_CLASS_ARRAY, ContextLoader.class, true); + assertAttributes(configAttributesList.get(2), testClass, new String[] { "C.xml" }, EMPTY_CLASS_ARRAY, + ContextLoader.class, true); } @Test @@ -97,6 +126,34 @@ public class ContextLoaderUtilsContextHierarchyTests extends AbstractContextLoad assertThat(configAttributesListClassLevel3.get(0).getLocations()[0], equalTo("three.xml")); } + @Test + public void resolveContextHierarchyAttributesForTestClassHierarchyWithSingleLevelContextHierarchiesAndMetaAnnotations() { + List> hierarchyAttributes = resolveContextHierarchyAttributes(TestClass3WithSingleLevelContextHierarchyFromMetaAnnotation.class); + assertEquals(3, hierarchyAttributes.size()); + + List configAttributesListClassLevel1 = hierarchyAttributes.get(0); + debugConfigAttributes(configAttributesListClassLevel1); + assertEquals(1, configAttributesListClassLevel1.size()); + assertThat(configAttributesListClassLevel1.get(0).getLocations()[0], equalTo("A.xml")); + assertAttributes(configAttributesListClassLevel1.get(0), ContextHierarchyA.class, new String[] { "A.xml" }, + EMPTY_CLASS_ARRAY, ContextLoader.class, true); + + List configAttributesListClassLevel2 = hierarchyAttributes.get(1); + debugConfigAttributes(configAttributesListClassLevel2); + assertEquals(1, configAttributesListClassLevel2.size()); + assertArrayEquals(new String[] { "B-one.xml", "B-two.xml" }, + configAttributesListClassLevel2.get(0).getLocations()); + assertAttributes(configAttributesListClassLevel2.get(0), ContextHierarchyB.class, new String[] { "B-one.xml", + "B-two.xml" }, EMPTY_CLASS_ARRAY, ContextLoader.class, true); + + List configAttributesListClassLevel3 = hierarchyAttributes.get(2); + debugConfigAttributes(configAttributesListClassLevel3); + assertEquals(1, configAttributesListClassLevel3.size()); + assertThat(configAttributesListClassLevel3.get(0).getLocations()[0], equalTo("C.xml")); + assertAttributes(configAttributesListClassLevel3.get(0), ContextHierarchyC.class, new String[] { "C.xml" }, + EMPTY_CLASS_ARRAY, ContextLoader.class, true); + } + @Test public void resolveContextHierarchyAttributesForTestClassHierarchyWithBareContextConfigurationInSubclass() { List> hierarchyAttributes = resolveContextHierarchyAttributes(TestClass2WithBareContextConfigurationInSubclass.class); @@ -301,6 +358,16 @@ public class ContextLoaderUtilsContextHierarchyTests extends AbstractContextLoad private static class SingleTestClassWithContextConfigurationAndContextHierarchy { } + @ContextConfiguration("foo.xml") + @ContextHierarchy(@ContextConfiguration("bar.xml")) + @Retention(RetentionPolicy.RUNTIME) + private static @interface ContextConfigurationAndContextHierarchyOnSingleMeta { + } + + @ContextConfigurationAndContextHierarchyOnSingleMeta + private static class SingleTestClassWithContextConfigurationAndContextHierarchyOnSingleMetaAnnotation { + } + @ContextHierarchy(@ContextConfiguration("A.xml")) private static class SingleTestClassWithSingleLevelContextHierarchy { } @@ -465,7 +532,41 @@ public class ContextLoaderUtilsContextHierarchyTests extends AbstractContextLoad public void initialize(ConfigurableApplicationContext applicationContext) { /* no-op */ } + } + // ------------------------------------------------------------------------- + + @ContextHierarchy(@ContextConfiguration("A.xml")) + @Retention(RetentionPolicy.RUNTIME) + private static @interface ContextHierarchyA { + } + + @ContextHierarchy(@ContextConfiguration({ "B-one.xml", "B-two.xml" })) + @Retention(RetentionPolicy.RUNTIME) + private static @interface ContextHierarchyB { + } + + @ContextHierarchy(@ContextConfiguration("C.xml")) + @Retention(RetentionPolicy.RUNTIME) + private static @interface ContextHierarchyC { + } + + @ContextHierarchyA + private static class SingleTestClassWithSingleLevelContextHierarchyFromMetaAnnotation { + } + + @ContextHierarchyA + private static class TestClass1WithSingleLevelContextHierarchyFromMetaAnnotation { + } + + @ContextHierarchyB + private static class TestClass2WithSingleLevelContextHierarchyFromMetaAnnotation extends + TestClass1WithSingleLevelContextHierarchyFromMetaAnnotation { + } + + @ContextHierarchyC + private static class TestClass3WithSingleLevelContextHierarchyFromMetaAnnotation extends + TestClass2WithSingleLevelContextHierarchyFromMetaAnnotation { } } diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsMergedConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsMergedConfigTests.java index c2db87bdf03..f9988392a4c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsMergedConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsMergedConfigTests.java @@ -31,9 +31,12 @@ import static org.springframework.test.context.ContextLoaderUtils.*; */ public class ContextLoaderUtilsMergedConfigTests extends AbstractContextLoaderUtilsTests { - @Test(expected = IllegalArgumentException.class) + @Test public void buildMergedConfigWithoutAnnotation() { - buildMergedContextConfiguration(Enigma.class, null, null); + Class testClass = Enigma.class; + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, null); } @Test @@ -57,6 +60,15 @@ public class ContextLoaderUtilsMergedConfigTests extends AbstractContextLoaderUt DelegatingSmartContextLoader.class); } + @Test + public void buildMergedConfigWithMetaAnnotationAndLocations() { + Class testClass = MetaLocationsFoo.class; + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + + assertMergedConfig(mergedConfig, testClass, new String[] { "classpath:/foo.xml" }, EMPTY_CLASS_ARRAY, + DelegatingSmartContextLoader.class); + } + @Test public void buildMergedConfigWithLocalAnnotationAndClasses() { Class testClass = ClassesFoo.class; diff --git a/spring-test/src/test/java/org/springframework/test/context/MetaAnnotationUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/MetaAnnotationUtilsTests.java new file mode 100644 index 00000000000..1b5b7c521db --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/MetaAnnotationUtilsTests.java @@ -0,0 +1,321 @@ +/* + * 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 java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.Test; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.test.context.MetaAnnotationUtils.AnnotationDescriptor; +import org.springframework.test.context.MetaAnnotationUtils.UntypedAnnotationDescriptor; +import org.springframework.transaction.annotation.Transactional; + +import static org.junit.Assert.*; +import static org.springframework.test.context.MetaAnnotationUtils.*; + +/** + * Unit tests for {@link MetaAnnotationUtils}. + * + * @author Sam Brannen + * @since 4.0 + */ +public class MetaAnnotationUtilsTests { + + private void assertComponentOnStereotype(Class startClass, Class declaringClass, String name, + Class stereotypeType) { + AnnotationDescriptor descriptor = findAnnotationDescriptor(startClass, Component.class); + assertNotNull(descriptor); + assertEquals(declaringClass, descriptor.getDeclaringClass()); + assertEquals(Component.class, descriptor.getAnnotationType()); + assertEquals(name, descriptor.getAnnotation().value()); + assertNotNull(descriptor.getStereotype()); + assertEquals(stereotypeType, descriptor.getStereotypeType()); + } + + @SuppressWarnings("unchecked") + private void assertComponentOnStereotypeForMultipleCandidateTypes(Class startClass, Class declaringClass, + String name, Class stereotypeType) { + Class annotationType = Component.class; + UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(startClass, Service.class, + annotationType, Order.class, Transactional.class); + assertNotNull(descriptor); + assertEquals(declaringClass, descriptor.getDeclaringClass()); + assertEquals(annotationType, descriptor.getAnnotationType()); + assertEquals(name, ((Component) descriptor.getAnnotation()).value()); + assertNotNull(descriptor.getStereotype()); + assertEquals(stereotypeType, descriptor.getStereotypeType()); + } + + @Test + public void findAnnotationDescriptorWithNoAnnotationPresent() throws Exception { + assertNull(findAnnotationDescriptor(NonAnnotatedInterface.class, Transactional.class)); + assertNull(findAnnotationDescriptor(NonAnnotatedClass.class, Transactional.class)); + } + + @Test + public void findAnnotationDescriptorWithInheritedAnnotationOnClass() throws Exception { + // Note: @Transactional is inherited + assertEquals(InheritedAnnotationClass.class, + findAnnotationDescriptor(InheritedAnnotationClass.class, Transactional.class).getDeclaringClass()); + assertEquals(InheritedAnnotationClass.class, + findAnnotationDescriptor(SubInheritedAnnotationClass.class, Transactional.class).getDeclaringClass()); + } + + @Test + public void findAnnotationDescriptorWithInheritedAnnotationOnInterface() throws Exception { + // Note: @Transactional is inherited + assertEquals(InheritedAnnotationInterface.class, + findAnnotationDescriptor(InheritedAnnotationInterface.class, Transactional.class).getDeclaringClass()); + assertNull(findAnnotationDescriptor(SubInheritedAnnotationInterface.class, Transactional.class)); + assertNull(findAnnotationDescriptor(SubSubInheritedAnnotationInterface.class, Transactional.class)); + } + + @Test + public void findAnnotationDescriptorForNonInheritedAnnotationOnClass() throws Exception { + // Note: @Order is not inherited. + assertEquals(NonInheritedAnnotationClass.class, + findAnnotationDescriptor(NonInheritedAnnotationClass.class, Order.class).getDeclaringClass()); + assertEquals(NonInheritedAnnotationClass.class, + findAnnotationDescriptor(SubNonInheritedAnnotationClass.class, Order.class).getDeclaringClass()); + } + + @Test + public void findAnnotationDescriptorForNonInheritedAnnotationOnInterface() throws Exception { + // Note: @Order is not inherited. + assertEquals(NonInheritedAnnotationInterface.class, + findAnnotationDescriptor(NonInheritedAnnotationInterface.class, Order.class).getDeclaringClass()); + assertNull(findAnnotationDescriptor(SubNonInheritedAnnotationInterface.class, Order.class)); + } + + @Test + public void findAnnotationDescriptorWithMetaComponentAnnotation() throws Exception { + Class startClass = HasMetaComponentAnnotation.class; + assertComponentOnStereotype(startClass, startClass, "meta1", Meta1.class); + } + + @Test + public void findAnnotationDescriptorWithLocalAndMetaComponentAnnotation() throws Exception { + Class annotationType = Component.class; + AnnotationDescriptor descriptor = findAnnotationDescriptor(HasLocalAndMetaComponentAnnotation.class, + annotationType); + assertEquals(HasLocalAndMetaComponentAnnotation.class, descriptor.getDeclaringClass()); + assertEquals(annotationType, descriptor.getAnnotationType()); + assertNull(descriptor.getStereotype()); + assertNull(descriptor.getStereotypeType()); + } + + @Test + public void findAnnotationDescriptorForInterfaceWithMetaAnnotation() { + Class startClass = InterfaceWithMetaAnnotation.class; + assertComponentOnStereotype(startClass, startClass, "meta1", Meta1.class); + } + + @Test + public void findAnnotationDescriptorForClassWithMetaAnnotatedInterface() { + assertNull(findAnnotationDescriptor(ClassWithMetaAnnotatedInterface.class, Component.class)); + } + + @Test + public void findAnnotationDescriptorForClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() { + Class startClass = ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class; + assertComponentOnStereotype(startClass, startClass, "meta2", Meta2.class); + } + + @Test + public void findAnnotationDescriptorForSubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() { + assertComponentOnStereotype(SubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, + ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, "meta2", Meta2.class); + } + + // ------------------------------------------------------------------------- + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesWithNoAnnotationPresent() throws Exception { + assertNull(findAnnotationDescriptorForTypes(NonAnnotatedInterface.class, Transactional.class, Component.class)); + assertNull(findAnnotationDescriptorForTypes(NonAnnotatedClass.class, Transactional.class, Order.class)); + } + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesWithInheritedAnnotationOnClass() throws Exception { + // Note: @Transactional is inherited + assertEquals(InheritedAnnotationClass.class, + findAnnotationDescriptorForTypes(InheritedAnnotationClass.class, Transactional.class).getDeclaringClass()); + assertEquals( + InheritedAnnotationClass.class, + findAnnotationDescriptorForTypes(SubInheritedAnnotationClass.class, Transactional.class).getDeclaringClass()); + } + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesWithInheritedAnnotationOnInterface() throws Exception { + // Note: @Transactional is inherited + assertEquals( + InheritedAnnotationInterface.class, + findAnnotationDescriptorForTypes(InheritedAnnotationInterface.class, Transactional.class).getDeclaringClass()); + assertNull(findAnnotationDescriptorForTypes(SubInheritedAnnotationInterface.class, Transactional.class)); + assertNull(findAnnotationDescriptorForTypes(SubSubInheritedAnnotationInterface.class, Transactional.class)); + } + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesForNonInheritedAnnotationOnClass() throws Exception { + // Note: @Order is not inherited. + assertEquals(NonInheritedAnnotationClass.class, + findAnnotationDescriptorForTypes(NonInheritedAnnotationClass.class, Order.class).getDeclaringClass()); + assertEquals(NonInheritedAnnotationClass.class, + findAnnotationDescriptorForTypes(SubNonInheritedAnnotationClass.class, Order.class).getDeclaringClass()); + } + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesForNonInheritedAnnotationOnInterface() throws Exception { + // Note: @Order is not inherited. + assertEquals(NonInheritedAnnotationInterface.class, + findAnnotationDescriptorForTypes(NonInheritedAnnotationInterface.class, Order.class).getDeclaringClass()); + assertNull(findAnnotationDescriptorForTypes(SubNonInheritedAnnotationInterface.class, Order.class)); + } + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesWithLocalAndMetaComponentAnnotation() throws Exception { + Class annotationType = Component.class; + UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes( + HasLocalAndMetaComponentAnnotation.class, Transactional.class, annotationType, Order.class); + assertEquals(HasLocalAndMetaComponentAnnotation.class, descriptor.getDeclaringClass()); + assertEquals(annotationType, descriptor.getAnnotationType()); + assertNull(descriptor.getStereotype()); + assertNull(descriptor.getStereotypeType()); + } + + @Test + public void findAnnotationDescriptorForTypesWithMetaComponentAnnotation() throws Exception { + Class startClass = HasMetaComponentAnnotation.class; + assertComponentOnStereotypeForMultipleCandidateTypes(startClass, startClass, "meta1", Meta1.class); + } + + @Test + public void findAnnotationDescriptorForTypesForInterfaceWithMetaAnnotation() { + Class startClass = InterfaceWithMetaAnnotation.class; + assertComponentOnStereotypeForMultipleCandidateTypes(startClass, startClass, "meta1", Meta1.class); + } + + @Test + @SuppressWarnings("unchecked") + public void findAnnotationDescriptorForTypesForClassWithMetaAnnotatedInterface() { + assertNull(findAnnotationDescriptorForTypes(ClassWithMetaAnnotatedInterface.class, Service.class, + Component.class, Order.class, Transactional.class)); + } + + @Test + public void findAnnotationDescriptorForTypesForClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() { + Class startClass = ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class; + assertComponentOnStereotypeForMultipleCandidateTypes(startClass, startClass, "meta2", Meta2.class); + } + + @Test + public void findAnnotationDescriptorForTypesForSubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface() { + assertComponentOnStereotypeForMultipleCandidateTypes( + SubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, + ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, "meta2", Meta2.class); + } + + + // ------------------------------------------------------------------------- + + @Component(value = "meta1") + @Order + @Retention(RetentionPolicy.RUNTIME) + static @interface Meta1 { + } + + @Component(value = "meta2") + @Transactional + @Retention(RetentionPolicy.RUNTIME) + static @interface Meta2 { + } + + @Meta1 + static class HasMetaComponentAnnotation { + } + + @Meta1 + @Component(value = "local") + @Meta2 + static class HasLocalAndMetaComponentAnnotation { + } + + @Meta1 + static interface InterfaceWithMetaAnnotation { + } + + static class ClassWithMetaAnnotatedInterface implements InterfaceWithMetaAnnotation { + } + + @Meta2 + static class ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface implements InterfaceWithMetaAnnotation { + } + + static class SubClassWithLocalMetaAnnotationAndMetaAnnotatedInterface extends + ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface { + } + + // ------------------------------------------------------------------------- + + @Transactional + static interface InheritedAnnotationInterface { + } + + static interface SubInheritedAnnotationInterface extends InheritedAnnotationInterface { + } + + static interface SubSubInheritedAnnotationInterface extends SubInheritedAnnotationInterface { + } + + @Order + static interface NonInheritedAnnotationInterface { + } + + static interface SubNonInheritedAnnotationInterface extends NonInheritedAnnotationInterface { + } + + static class NonAnnotatedClass { + } + + static interface NonAnnotatedInterface { + } + + @Transactional + static class InheritedAnnotationClass { + } + + static class SubInheritedAnnotationClass extends InheritedAnnotationClass { + } + + @Order + static class NonInheritedAnnotationClass { + } + + static class SubNonInheritedAnnotationClass extends NonInheritedAnnotationClass { + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java index 5596e6ea8bf..6fd55d2b8e6 100644 --- a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java @@ -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. @@ -18,6 +18,9 @@ package org.springframework.test.context; import static org.junit.Assert.assertEquals; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + import org.junit.Test; import org.springframework.test.context.support.AbstractTestExecutionListener; @@ -42,7 +45,7 @@ public class TestExecutionListenersTests { @Test public void verifyNumDefaultListenersRegistered() throws Exception { TestContextManager testContextManager = new TestContextManager(DefaultListenersExampleTestCase.class); - assertEquals("Verifying the number of registered TestExecutionListeners for DefaultListenersExampleTest.", 4, + assertEquals("Num registered TELs for DefaultListenersExampleTestCase.", 4, testContextManager.getTestExecutionListeners().size()); } @@ -50,47 +53,64 @@ public class TestExecutionListenersTests { public void verifyNumNonInheritedDefaultListenersRegistered() throws Exception { TestContextManager testContextManager = new TestContextManager( NonInheritedDefaultListenersExampleTestCase.class); - assertEquals( - "Verifying the number of registered TestExecutionListeners for NonInheritedDefaultListenersExampleTest.", - 1, testContextManager.getTestExecutionListeners().size()); + assertEquals("Num registered TELs for NonInheritedDefaultListenersExampleTestCase.", 1, + testContextManager.getTestExecutionListeners().size()); } @Test public void verifyNumInheritedDefaultListenersRegistered() throws Exception { TestContextManager testContextManager = new TestContextManager(InheritedDefaultListenersExampleTestCase.class); - assertEquals( - "Verifying the number of registered TestExecutionListeners for InheritedDefaultListenersExampleTest.", 1, + assertEquals("Num registered TELs for InheritedDefaultListenersExampleTestCase.", 1, testContextManager.getTestExecutionListeners().size()); testContextManager = new TestContextManager(SubInheritedDefaultListenersExampleTestCase.class); - assertEquals( - "Verifying the number of registered TestExecutionListeners for SubInheritedDefaultListenersExampleTest.", - 1, testContextManager.getTestExecutionListeners().size()); + assertEquals("Num registered TELs for SubInheritedDefaultListenersExampleTestCase.", 1, + testContextManager.getTestExecutionListeners().size()); testContextManager = new TestContextManager(SubSubInheritedDefaultListenersExampleTestCase.class); - assertEquals( - "Verifying the number of registered TestExecutionListeners for SubSubInheritedDefaultListenersExampleTest.", - 2, testContextManager.getTestExecutionListeners().size()); + assertEquals("Num registered TELs for SubSubInheritedDefaultListenersExampleTestCase.", 2, + testContextManager.getTestExecutionListeners().size()); } @Test public void verifyNumListenersRegistered() throws Exception { TestContextManager testContextManager = new TestContextManager(ExampleTestCase.class); - assertEquals("Verifying the number of registered TestExecutionListeners for ExampleTest.", 3, + assertEquals("Num registered TELs for ExampleTestCase.", 3, testContextManager.getTestExecutionListeners().size()); } @Test public void verifyNumNonInheritedListenersRegistered() throws Exception { TestContextManager testContextManager = new TestContextManager(NonInheritedListenersExampleTestCase.class); - assertEquals("Verifying the number of registered TestExecutionListeners for NonInheritedListenersExampleTest.", - 1, testContextManager.getTestExecutionListeners().size()); + assertEquals("Num registered TELs for NonInheritedListenersExampleTestCase.", 1, + testContextManager.getTestExecutionListeners().size()); } @Test public void verifyNumInheritedListenersRegistered() throws Exception { TestContextManager testContextManager = new TestContextManager(InheritedListenersExampleTestCase.class); - assertEquals("Verifying the number of registered TestExecutionListeners for InheritedListenersExampleTest.", 4, + assertEquals("Num registered TELs for InheritedListenersExampleTestCase.", 4, + testContextManager.getTestExecutionListeners().size()); + } + + @Test + public void verifyNumListenersRegisteredViaMetaAnnotation() throws Exception { + TestContextManager testContextManager = new TestContextManager(MetaExampleTestCase.class); + assertEquals("Num registered TELs for MetaExampleTestCase.", 3, + testContextManager.getTestExecutionListeners().size()); + } + + @Test + public void verifyNumNonInheritedListenersRegisteredViaMetaAnnotation() throws Exception { + TestContextManager testContextManager = new TestContextManager(MetaNonInheritedListenersExampleTestCase.class); + assertEquals("Num registered TELs for MetaNonInheritedListenersExampleTestCase.", 1, + testContextManager.getTestExecutionListeners().size()); + } + + @Test + public void verifyNumInheritedListenersRegisteredViaMetaAnnotation() throws Exception { + TestContextManager testContextManager = new TestContextManager(MetaInheritedListenersExampleTestCase.class); + assertEquals("Num registered TELs for MetaInheritedListenersExampleTestCase.", 4, testContextManager.getTestExecutionListeners().size()); } @@ -118,7 +138,7 @@ public class TestExecutionListenersTests { static class NonInheritedDefaultListenersExampleTestCase extends InheritedDefaultListenersExampleTestCase { } - @TestExecutionListeners( { FooTestExecutionListener.class, BarTestExecutionListener.class, + @TestExecutionListeners({ FooTestExecutionListener.class, BarTestExecutionListener.class, BazTestExecutionListener.class }) static class ExampleTestCase { } @@ -135,6 +155,37 @@ public class TestExecutionListenersTests { static class DuplicateListenersConfigExampleTestCase { } + @TestExecutionListeners({// + FooTestExecutionListener.class,// + BarTestExecutionListener.class,// + BazTestExecutionListener.class // + }) + @Retention(RetentionPolicy.RUNTIME) + static @interface MetaListeners { + } + + @TestExecutionListeners(QuuxTestExecutionListener.class) + @Retention(RetentionPolicy.RUNTIME) + static @interface MetaInheritedListeners { + } + + @TestExecutionListeners(listeners = QuuxTestExecutionListener.class, inheritListeners = false) + @Retention(RetentionPolicy.RUNTIME) + static @interface MetaNonInheritedListeners { + } + + @MetaListeners + static class MetaExampleTestCase { + } + + @MetaInheritedListeners + static class MetaInheritedListenersExampleTestCase extends MetaExampleTestCase { + } + + @MetaNonInheritedListeners + static class MetaNonInheritedListenersExampleTestCase extends MetaInheritedListenersExampleTestCase { + } + static class FooTestExecutionListener extends AbstractTestExecutionListener { } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/RepeatedSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/RepeatedSpringRunnerTests.java index 6b7241261be..68d0d01f2d7 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/RepeatedSpringRunnerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/RepeatedSpringRunnerTests.java @@ -19,6 +19,8 @@ package org.springframework.test.context.junit4; import static org.junit.Assert.*; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.Collection; import java.util.concurrent.atomic.AtomicInteger; @@ -77,6 +79,7 @@ public class RepeatedSpringRunnerTests { { DefaultRepeatValueRepeatedTestCase.class, 0, 1, 1, 1 },// { NegativeRepeatValueRepeatedTestCase.class, 0, 1, 1, 1 },// { RepeatedFiveTimesRepeatedTestCase.class, 0, 1, 1, 5 },// + { RepeatedFiveTimesViaMetaAnnotationRepeatedTestCase.class, 0, 1, 1, 5 },// { TimedRepeatedTestCase.class, 3, 4, 4, (5 + 1 + 4 + 10) } // }); } @@ -147,6 +150,20 @@ public class RepeatedSpringRunnerTests { } } + @Repeat(5) + @Retention(RetentionPolicy.RUNTIME) + private static @interface RepeatedFiveTimes { + } + + public static final class RepeatedFiveTimesViaMetaAnnotationRepeatedTestCase extends AbstractRepeatedTestCase { + + @Test + @RepeatedFiveTimes + public void repeatedFiveTimes() throws Exception { + incrementInvocationCount(); + } + } + /** * Unit tests for claims raised in clazz = getClass(); + Mockito.> when(testContext.getTestClass()).thenReturn(clazz); + when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("dirtiesContextDeclaredLocally")); + listener.afterTestMethod(testContext); + verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); + } + + @Test + public void afterTestMethodForDirtiesContextDeclaredOnMethodViaMetaAnnotation() throws Exception { + Class clazz = getClass(); + Mockito.> when(testContext.getTestClass()).thenReturn(clazz); + when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("dirtiesContextDeclaredViaMetaAnnotation")); + listener.afterTestMethod(testContext); + verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); + } + + @Test + public void afterTestMethodForDirtiesContextDeclaredLocallyOnClassAfterEachTestMethod() throws Exception { + Class clazz = DirtiesContextDeclaredLocallyAfterEachTestMethod.class; + Mockito.> when(testContext.getTestClass()).thenReturn(clazz); + when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("clean")); + listener.afterTestMethod(testContext); + verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); + } + + @Test + public void afterTestMethodForDirtiesContextDeclaredViaMetaAnnotationOnClassAfterEachTestMethod() throws Exception { + Class clazz = DirtiesContextDeclaredViaMetaAnnotationAfterEachTestMethod.class; + Mockito.> when(testContext.getTestClass()).thenReturn(clazz); + when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("clean")); + listener.afterTestMethod(testContext); + verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); + } + + @Test + public void afterTestMethodForDirtiesContextDeclaredLocallyOnClassAfterClass() throws Exception { + Class clazz = DirtiesContextDeclaredLocallyAfterClass.class; + Mockito.> when(testContext.getTestClass()).thenReturn(clazz); + when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("clean")); + listener.afterTestMethod(testContext); + verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); + } + + @Test + public void afterTestMethodForDirtiesContextDeclaredViaMetaAnnotationOnClassAfterClass() throws Exception { + Class clazz = DirtiesContextDeclaredViaMetaAnnotationAfterClass.class; + Mockito.> when(testContext.getTestClass()).thenReturn(clazz); + when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("clean")); + listener.afterTestMethod(testContext); + verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); + } + + // ------------------------------------------------------------------------- + + @Test + public void afterTestClassForDirtiesContextDeclaredLocallyOnMethod() throws Exception { + Class clazz = getClass(); + Mockito.> when(testContext.getTestClass()).thenReturn(clazz); + listener.afterTestClass(testContext); + verify(testContext, times(0)).markApplicationContextDirty(any(HierarchyMode.class)); + } + + @Test + public void afterTestClassForDirtiesContextDeclaredLocallyOnClassAfterEachTestMethod() throws Exception { + Class clazz = DirtiesContextDeclaredLocallyAfterEachTestMethod.class; + Mockito.> when(testContext.getTestClass()).thenReturn(clazz); + listener.afterTestClass(testContext); + verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); + } + + @Test + public void afterTestClassForDirtiesContextDeclaredViaMetaAnnotationOnClassAfterEachTestMethod() throws Exception { + Class clazz = DirtiesContextDeclaredViaMetaAnnotationAfterEachTestMethod.class; + Mockito.> when(testContext.getTestClass()).thenReturn(clazz); + listener.afterTestClass(testContext); + verify(testContext, times(1)).markApplicationContextDirty(EXHAUSTIVE); + } + + @Test + public void afterTestClassForDirtiesContextDeclaredLocallyOnClassAfterClass() throws Exception { + Class clazz = DirtiesContextDeclaredLocallyAfterClass.class; + Mockito.> when(testContext.getTestClass()).thenReturn(clazz); + listener.afterTestClass(testContext); + verify(testContext, times(1)).markApplicationContextDirty(any(HierarchyMode.class)); + } + + @Test + public void afterTestClassForDirtiesContextDeclaredViaMetaAnnotationOnClassAfterClass() throws Exception { + Class clazz = DirtiesContextDeclaredViaMetaAnnotationAfterClass.class; + Mockito.> when(testContext.getTestClass()).thenReturn(clazz); + listener.afterTestClass(testContext); + verify(testContext, times(1)).markApplicationContextDirty(any(HierarchyMode.class)); + } + + // ------------------------------------------------------------------------- + + @DirtiesContext + void dirtiesContextDeclaredLocally() { + /* no-op */ + } + + @MetaDirty + void dirtiesContextDeclaredViaMetaAnnotation() { + /* no-op */ + } + + + @DirtiesContext + @Retention(RetentionPolicy.RUNTIME) + static @interface MetaDirty { + } + + @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) + @Retention(RetentionPolicy.RUNTIME) + static @interface MetaDirtyAfterEachTestMethod { + } + + @DirtiesContext(classMode = ClassMode.AFTER_CLASS) + @Retention(RetentionPolicy.RUNTIME) + static @interface MetaDirtyAfterClass { + } + + @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) + static class DirtiesContextDeclaredLocallyAfterEachTestMethod { + + void clean() { + /* no-op */ + } + } + + @MetaDirtyAfterEachTestMethod + static class DirtiesContextDeclaredViaMetaAnnotationAfterEachTestMethod { + + void clean() { + /* no-op */ + } + } + + @DirtiesContext(classMode = ClassMode.AFTER_CLASS) + static class DirtiesContextDeclaredLocallyAfterClass { + + void clean() { + /* no-op */ + } + } + + @MetaDirtyAfterClass + static class DirtiesContextDeclaredViaMetaAnnotationAfterClass { + + void clean() { + /* no-op */ + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/web/MetaAnnotationConfigWacTests.java b/spring-test/src/test/java/org/springframework/test/context/web/MetaAnnotationConfigWacTests.java new file mode 100644 index 00000000000..7bd62b0b762 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/web/MetaAnnotationConfigWacTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2012 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.web; + +import java.io.File; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.context.WebApplicationContext; + +import static org.junit.Assert.*; + +/** + * Integration test that verifies meta-annotation support for {@link WebAppConfiguration} + * and {@link org.springframework.test.context.ContextConfiguration ContextConfiguration}. + * + * @author Sam Brannen + * @since 4.0 + * @see WebTests + */ +@RunWith(SpringJUnit4ClassRunner.class) +@WebTests +public class MetaAnnotationConfigWacTests { + + @Autowired + protected WebApplicationContext wac; + + @Autowired + protected MockServletContext mockServletContext; + + @Autowired + protected String foo; + + + @Test + public void fooEnigmaAutowired() { + assertEquals("enigma", foo); + } + + @Test + public void basicWacFeatures() throws Exception { + assertNotNull("ServletContext should be set in the WAC.", wac.getServletContext()); + + assertNotNull("ServletContext should have been autowired from the WAC.", mockServletContext); + + Object rootWac = mockServletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); + assertNotNull("Root WAC must be stored in the ServletContext as: " + + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, rootWac); + assertSame("test WAC and Root WAC in ServletContext must be the same object.", wac, rootWac); + assertSame("ServletContext instances must be the same object.", mockServletContext, wac.getServletContext()); + + assertEquals("Getting real path for ServletContext resource.", + new File("src/main/webapp/index.jsp").getCanonicalPath(), mockServletContext.getRealPath("index.jsp")); + } + +} \ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/context/web/WebTests.java b/spring-test/src/test/java/org/springframework/test/context/web/WebTests.java new file mode 100644 index 00000000000..604dca2e4b2 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/web/WebTests.java @@ -0,0 +1,46 @@ +/* + * 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.web; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +/** + * Custom stereotype combining {@link WebAppConfiguration} and + * {@link ContextConfiguration} as meta-annotations. + * + * @author Sam Brannen + * @since 4.0 + */ +@WebAppConfiguration +@ContextConfiguration +@Retention(RetentionPolicy.RUNTIME) +public @interface WebTests { + + @Configuration + static class Config { + + @Bean + public String foo() { + return "enigma"; + } + } +} \ No newline at end of file