Support test annotations on interfaces

Prior to Java 8 it never really made much sense to author integration
tests using interfaces. Consequently, the Spring TestContext Framework
has never supported finding test-related annotations on interfaces in
its search algorithms.

However, Java 8's support for interface default methods introduces new
testing use cases for which it makes sense to declare test
configuration (e.g., @ContextConfiguration, etc.) on an interface
containing default methods instead of on an abstract base class.

This commit ensures that all non-repeatable, class-level test
annotations in the Spring TestContext Framework can now be declared on
test interfaces. The only test annotations that cannot be declared on
interfaces are therefore @Sql and @SqlGroup.

Issue: SPR-14184
This commit is contained in:
Sam Brannen
2016-05-05 19:51:21 +02:00
parent a31f0bb3c0
commit a1a87679da
34 changed files with 973 additions and 84 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 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,7 @@ package org.springframework.test.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@@ -51,6 +52,7 @@ import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Rollback(false)
public @interface Commit {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2016 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.
@@ -24,7 +24,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* <p>{@code ProfileValueSourceConfiguration} is a class-level annotation which
* {@code ProfileValueSourceConfiguration} is a class-level annotation which
* is used to specify what type of {@link ProfileValueSource} to use when
* retrieving <em>profile values</em> configured via the {@link IfProfileValue
* &#064;IfProfileValue} annotation.
@@ -38,17 +38,15 @@ import java.lang.annotation.Target;
* @see IfProfileValue
* @see ProfileValueUtils
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ProfileValueSourceConfiguration {
/**
* <p>
* The type of {@link ProfileValueSource} to use when retrieving
* <em>profile values</em>.
* </p>
*
* @see SystemProfileValueSource
*/

View File

@@ -18,6 +18,7 @@ package org.springframework.test.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@@ -56,6 +57,7 @@ import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Rollback {
/**

View File

@@ -43,10 +43,10 @@ import org.springframework.core.annotation.AliasFor;
* @see org.springframework.context.ApplicationContext
* @see org.springframework.context.annotation.Profile
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ActiveProfiles {
/**

View File

@@ -17,16 +17,15 @@
package org.springframework.test.context;
import java.lang.reflect.Constructor;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.util.ClassUtils;
import org.springframework.util.MultiValueMap;
/**
* {@code BootstrapUtils} is a collection of utility methods to assist with
@@ -151,17 +150,16 @@ abstract class BootstrapUtils {
* @since 4.3
*/
private static Class<?> resolveExplicitTestContextBootstrapper(Class<?> testClass) {
MultiValueMap<String, Object> attributesMultiMap =
AnnotatedElementUtils.getAllAnnotationAttributes(testClass, BootstrapWith.class.getName());
List<Object> values = (attributesMultiMap != null ? attributesMultiMap.get(AnnotationUtils.VALUE) : null);
if (values == null) {
Set<BootstrapWith> annotations = AnnotatedElementUtils.findAllMergedAnnotations(testClass, BootstrapWith.class);
if (annotations.size() < 1) {
return null;
}
if (values.size() != 1) {
throw new IllegalStateException(String.format("Configuration error: found multiple declarations of " +
"@BootstrapWith on test class [%s] with values %s", testClass.getName(), values));
if (annotations.size() > 1) {
throw new IllegalStateException(String.format(
"Configuration error: found multiple declarations of @BootstrapWith for test class [%s]: %s",
testClass.getName(), annotations));
}
return (Class<?>) values.get(0);
return annotations.iterator().next().value();
}
/**
@@ -169,7 +167,9 @@ abstract class BootstrapUtils {
*/
private static Class<?> resolveDefaultTestContextBootstrapper(Class<?> testClass) throws Exception {
ClassLoader classLoader = BootstrapUtils.class.getClassLoader();
if (AnnotatedElementUtils.isAnnotated(testClass, WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME)) {
AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(testClass,
WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME, false, false);
if (attributes != null) {
return ClassUtils.forName(DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, classLoader);
}
return ClassUtils.forName(DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, classLoader);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2016 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.
@@ -35,10 +35,10 @@ import java.lang.annotation.Target;
* @see BootstrapContext
* @see TestContextBootstrapper
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface BootstrapWith {
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2016 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.
@@ -83,10 +83,10 @@ import org.springframework.core.annotation.AliasFor;
* @see MergedContextConfiguration
* @see org.springframework.context.ApplicationContext
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ContextConfiguration {
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 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.
@@ -139,10 +139,10 @@ import java.lang.annotation.Target;
* @see ContextConfiguration
* @see org.springframework.context.ApplicationContext
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ContextHierarchy {
/**

View File

@@ -42,10 +42,10 @@ import org.springframework.core.annotation.AliasFor;
* @see TestContextManager
* @see ContextConfiguration
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TestExecutionListeners {
/**

View File

@@ -82,10 +82,10 @@ import org.springframework.core.annotation.AliasFor;
* @see org.springframework.core.env.PropertySource
* @see org.springframework.context.annotation.PropertySource
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TestPropertySource {
/**

View File

@@ -57,8 +57,8 @@ public abstract class MetaAnnotationUtils {
/**
* Find the {@link AnnotationDescriptor} for the supplied {@code annotationType}
* on the supplied {@link Class}, traversing its annotations and superclasses
* if no annotation can be found on the given class itself.
* on the supplied {@link Class}, traversing its annotations, interfaces, and
* superclasses if no annotation can be found on the given class itself.
* <p>This method explicitly handles class-level annotations which are not
* declared as {@linkplain java.lang.annotation.Inherited inherited} <em>as
* well as meta-annotations</em>.
@@ -67,14 +67,12 @@ public abstract class MetaAnnotationUtils {
* <li>Search for the annotation on the given class and return a corresponding
* {@code AnnotationDescriptor} if found.
* <li>Recursively search through all annotations that the given class declares.
* <li>Recursively search through all interfaces implemented by the given class.
* <li>Recursively search through the superclass hierarchy of the given class.
* </ol>
* <p>In this context, the term <em>recursively</em> means that the search
* process continues by returning to step #1 with the current annotation or
* superclass as the class to look for annotations on.
* <p>If the supplied {@code clazz} is an interface, only the interface
* itself will be checked; the inheritance hierarchy for interfaces will not
* be traversed.
* process continues by returning to step #1 with the current annotation,
* interface, or superclass as the class to look for annotations on.
* @param clazz the class to look for annotations on
* @param annotationType the type of annotation to look for
* @return the corresponding annotation descriptor if the annotation was found;
@@ -123,6 +121,15 @@ public abstract class MetaAnnotationUtils {
}
}
// Declared on interface?
for (Class<?> ifc : clazz.getInterfaces()) {
AnnotationDescriptor<T> descriptor = findAnnotationDescriptor(ifc, visited, annotationType);
if (descriptor != null) {
return new AnnotationDescriptor<T>(clazz, descriptor.getDeclaringClass(),
descriptor.getComposedAnnotation(), descriptor.getAnnotation());
}
}
// Declared on a superclass?
return findAnnotationDescriptor(clazz.getSuperclass(), visited, annotationType);
}
@@ -132,8 +139,9 @@ public abstract class MetaAnnotationUtils {
* in the inheritance hierarchy of the specified {@code clazz} (including
* the specified {@code clazz} itself) which declares at least one of the
* specified {@code annotationTypes}.
* <p>This method traverses the annotations and superclasses of the specified
* {@code clazz} if no annotation can be found on the given class itself.
* <p>This method traverses the annotations, interfaces, and superclasses
* of the specified {@code clazz} if no annotation can be found on the given
* class itself.
* <p>This method explicitly handles class-level annotations which are not
* declared as {@linkplain java.lang.annotation.Inherited inherited} <em>as
* well as meta-annotations</em>.
@@ -143,14 +151,12 @@ public abstract class MetaAnnotationUtils {
* the given class and return a corresponding {@code UntypedAnnotationDescriptor}
* if found.
* <li>Recursively search through all annotations that the given class declares.
* <li>Recursively search through all interfaces implemented by the given class.
* <li>Recursively search through the superclass hierarchy of the given class.
* </ol>
* <p>In this context, the term <em>recursively</em> means that the search
* process continues by returning to step #1 with the current annotation or
* superclass as the class to look for annotations on.
* <p>If the supplied {@code clazz} is an interface, only the interface
* itself will be checked; the inheritance hierarchy for interfaces will not
* be traversed.
* process continues by returning to step #1 with the current annotation,
* interface, or superclass as the class to look for annotations on.
* @param clazz the class to look for annotations on
* @param annotationTypes the types of annotations to look for
* @return the corresponding annotation descriptor if one of the annotations
@@ -203,6 +209,15 @@ public abstract class MetaAnnotationUtils {
}
}
// Declared on interface?
for (Class<?> ifc : clazz.getInterfaces()) {
UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(ifc, visited, annotationTypes);
if (descriptor != null) {
return new UntypedAnnotationDescriptor(clazz, descriptor.getDeclaringClass(),
descriptor.getComposedAnnotation(), descriptor.getAnnotation());
}
}
// Declared on a superclass?
return findAnnotationDescriptorForTypes(clazz.getSuperclass(), visited, annotationTypes);
}