Skip to content

Commit a1a8767

Browse files
committed
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
1 parent a31f0bb commit a1a8767

34 files changed

+973
-84
lines changed

spring-context/src/test/java/org/springframework/tests/sample/beans/Employee.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,13 @@ public class Employee extends TestBean {
2020

2121
private String co;
2222

23+
public Employee() {
24+
}
25+
26+
public Employee(String name) {
27+
super(name);
28+
}
29+
2330
public String getCompany() {
2431
return co;
2532
}

spring-test/src/main/java/org/springframework/test/annotation/Commit.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818

1919
import java.lang.annotation.Documented;
2020
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Inherited;
2122
import java.lang.annotation.Retention;
2223
import java.lang.annotation.RetentionPolicy;
2324
import java.lang.annotation.Target;
@@ -51,6 +52,7 @@
5152
@Target({ElementType.TYPE, ElementType.METHOD})
5253
@Retention(RetentionPolicy.RUNTIME)
5354
@Documented
55+
@Inherited
5456
@Rollback(false)
5557
public @interface Commit {
5658
}

spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,7 +24,7 @@
2424
import java.lang.annotation.Target;
2525

2626
/**
27-
* <p>{@code ProfileValueSourceConfiguration} is a class-level annotation which
27+
* {@code ProfileValueSourceConfiguration} is a class-level annotation which
2828
* is used to specify what type of {@link ProfileValueSource} to use when
2929
* retrieving <em>profile values</em> configured via the {@link IfProfileValue
3030
* &#064;IfProfileValue} annotation.
@@ -38,17 +38,15 @@
3838
* @see IfProfileValue
3939
* @see ProfileValueUtils
4040
*/
41+
@Target(ElementType.TYPE)
42+
@Retention(RetentionPolicy.RUNTIME)
4143
@Documented
4244
@Inherited
43-
@Retention(RetentionPolicy.RUNTIME)
44-
@Target(ElementType.TYPE)
4545
public @interface ProfileValueSourceConfiguration {
4646

4747
/**
48-
* <p>
4948
* The type of {@link ProfileValueSource} to use when retrieving
5049
* <em>profile values</em>.
51-
* </p>
5250
*
5351
* @see SystemProfileValueSource
5452
*/

spring-test/src/main/java/org/springframework/test/annotation/Rollback.java

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.lang.annotation.Documented;
2020
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Inherited;
2122
import java.lang.annotation.Retention;
2223
import java.lang.annotation.RetentionPolicy;
2324
import java.lang.annotation.Target;
@@ -56,6 +57,7 @@
5657
@Target({ElementType.TYPE, ElementType.METHOD})
5758
@Retention(RetentionPolicy.RUNTIME)
5859
@Documented
60+
@Inherited
5961
public @interface Rollback {
6062

6163
/**

spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@
4343
* @see org.springframework.context.ApplicationContext
4444
* @see org.springframework.context.annotation.Profile
4545
*/
46+
@Target(ElementType.TYPE)
47+
@Retention(RetentionPolicy.RUNTIME)
4648
@Documented
4749
@Inherited
48-
@Retention(RetentionPolicy.RUNTIME)
49-
@Target(ElementType.TYPE)
5050
public @interface ActiveProfiles {
5151

5252
/**

spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java

+12-12
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,15 @@
1717
package org.springframework.test.context;
1818

1919
import java.lang.reflect.Constructor;
20-
import java.util.List;
20+
import java.util.Set;
2121

2222
import org.apache.commons.logging.Log;
2323
import org.apache.commons.logging.LogFactory;
2424

2525
import org.springframework.beans.BeanUtils;
2626
import org.springframework.core.annotation.AnnotatedElementUtils;
27-
import org.springframework.core.annotation.AnnotationUtils;
27+
import org.springframework.core.annotation.AnnotationAttributes;
2828
import org.springframework.util.ClassUtils;
29-
import org.springframework.util.MultiValueMap;
3029

3130
/**
3231
* {@code BootstrapUtils} is a collection of utility methods to assist with
@@ -151,25 +150,26 @@ static TestContextBootstrapper resolveTestContextBootstrapper(BootstrapContext b
151150
* @since 4.3
152151
*/
153152
private static Class<?> resolveExplicitTestContextBootstrapper(Class<?> testClass) {
154-
MultiValueMap<String, Object> attributesMultiMap =
155-
AnnotatedElementUtils.getAllAnnotationAttributes(testClass, BootstrapWith.class.getName());
156-
List<Object> values = (attributesMultiMap != null ? attributesMultiMap.get(AnnotationUtils.VALUE) : null);
157-
if (values == null) {
153+
Set<BootstrapWith> annotations = AnnotatedElementUtils.findAllMergedAnnotations(testClass, BootstrapWith.class);
154+
if (annotations.size() < 1) {
158155
return null;
159156
}
160-
if (values.size() != 1) {
161-
throw new IllegalStateException(String.format("Configuration error: found multiple declarations of " +
162-
"@BootstrapWith on test class [%s] with values %s", testClass.getName(), values));
157+
if (annotations.size() > 1) {
158+
throw new IllegalStateException(String.format(
159+
"Configuration error: found multiple declarations of @BootstrapWith for test class [%s]: %s",
160+
testClass.getName(), annotations));
163161
}
164-
return (Class<?>) values.get(0);
162+
return annotations.iterator().next().value();
165163
}
166164

167165
/**
168166
* @since 4.3
169167
*/
170168
private static Class<?> resolveDefaultTestContextBootstrapper(Class<?> testClass) throws Exception {
171169
ClassLoader classLoader = BootstrapUtils.class.getClassLoader();
172-
if (AnnotatedElementUtils.isAnnotated(testClass, WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME)) {
170+
AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(testClass,
171+
WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME, false, false);
172+
if (attributes != null) {
173173
return ClassUtils.forName(DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, classLoader);
174174
}
175175
return ClassUtils.forName(DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, classLoader);

spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -35,10 +35,10 @@
3535
* @see BootstrapContext
3636
* @see TestContextBootstrapper
3737
*/
38+
@Target(ElementType.TYPE)
39+
@Retention(RetentionPolicy.RUNTIME)
3840
@Documented
3941
@Inherited
40-
@Retention(RetentionPolicy.RUNTIME)
41-
@Target(ElementType.TYPE)
4242
public @interface BootstrapWith {
4343

4444
/**

spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -83,10 +83,10 @@
8383
* @see MergedContextConfiguration
8484
* @see org.springframework.context.ApplicationContext
8585
*/
86+
@Target(ElementType.TYPE)
87+
@Retention(RetentionPolicy.RUNTIME)
8688
@Documented
8789
@Inherited
88-
@Retention(RetentionPolicy.RUNTIME)
89-
@Target(ElementType.TYPE)
9090
public @interface ContextConfiguration {
9191

9292
/**

spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -139,10 +139,10 @@
139139
* @see ContextConfiguration
140140
* @see org.springframework.context.ApplicationContext
141141
*/
142+
@Target(ElementType.TYPE)
143+
@Retention(RetentionPolicy.RUNTIME)
142144
@Documented
143145
@Inherited
144-
@Retention(RetentionPolicy.RUNTIME)
145-
@Target(ElementType.TYPE)
146146
public @interface ContextHierarchy {
147147

148148
/**

spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@
4242
* @see TestContextManager
4343
* @see ContextConfiguration
4444
*/
45+
@Target(ElementType.TYPE)
46+
@Retention(RetentionPolicy.RUNTIME)
4547
@Documented
4648
@Inherited
47-
@Retention(RetentionPolicy.RUNTIME)
48-
@Target(ElementType.TYPE)
4949
public @interface TestExecutionListeners {
5050

5151
/**

spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,10 @@
8282
* @see org.springframework.core.env.PropertySource
8383
* @see org.springframework.context.annotation.PropertySource
8484
*/
85+
@Target(ElementType.TYPE)
86+
@Retention(RetentionPolicy.RUNTIME)
8587
@Documented
8688
@Inherited
87-
@Retention(RetentionPolicy.RUNTIME)
88-
@Target(ElementType.TYPE)
8989
public @interface TestPropertySource {
9090

9191
/**

spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java

+29-14
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ public abstract class MetaAnnotationUtils {
5757

5858
/**
5959
* Find the {@link AnnotationDescriptor} for the supplied {@code annotationType}
60-
* on the supplied {@link Class}, traversing its annotations and superclasses
61-
* if no annotation can be found on the given class itself.
60+
* on the supplied {@link Class}, traversing its annotations, interfaces, and
61+
* superclasses if no annotation can be found on the given class itself.
6262
* <p>This method explicitly handles class-level annotations which are not
6363
* declared as {@linkplain java.lang.annotation.Inherited inherited} <em>as
6464
* well as meta-annotations</em>.
@@ -67,14 +67,12 @@ public abstract class MetaAnnotationUtils {
6767
* <li>Search for the annotation on the given class and return a corresponding
6868
* {@code AnnotationDescriptor} if found.
6969
* <li>Recursively search through all annotations that the given class declares.
70+
* <li>Recursively search through all interfaces implemented by the given class.
7071
* <li>Recursively search through the superclass hierarchy of the given class.
7172
* </ol>
7273
* <p>In this context, the term <em>recursively</em> means that the search
73-
* process continues by returning to step #1 with the current annotation or
74-
* superclass as the class to look for annotations on.
75-
* <p>If the supplied {@code clazz} is an interface, only the interface
76-
* itself will be checked; the inheritance hierarchy for interfaces will not
77-
* be traversed.
74+
* process continues by returning to step #1 with the current annotation,
75+
* interface, or superclass as the class to look for annotations on.
7876
* @param clazz the class to look for annotations on
7977
* @param annotationType the type of annotation to look for
8078
* @return the corresponding annotation descriptor if the annotation was found;
@@ -123,6 +121,15 @@ private static <T extends Annotation> AnnotationDescriptor<T> findAnnotationDesc
123121
}
124122
}
125123

124+
// Declared on interface?
125+
for (Class<?> ifc : clazz.getInterfaces()) {
126+
AnnotationDescriptor<T> descriptor = findAnnotationDescriptor(ifc, visited, annotationType);
127+
if (descriptor != null) {
128+
return new AnnotationDescriptor<T>(clazz, descriptor.getDeclaringClass(),
129+
descriptor.getComposedAnnotation(), descriptor.getAnnotation());
130+
}
131+
}
132+
126133
// Declared on a superclass?
127134
return findAnnotationDescriptor(clazz.getSuperclass(), visited, annotationType);
128135
}
@@ -132,8 +139,9 @@ private static <T extends Annotation> AnnotationDescriptor<T> findAnnotationDesc
132139
* in the inheritance hierarchy of the specified {@code clazz} (including
133140
* the specified {@code clazz} itself) which declares at least one of the
134141
* specified {@code annotationTypes}.
135-
* <p>This method traverses the annotations and superclasses of the specified
136-
* {@code clazz} if no annotation can be found on the given class itself.
142+
* <p>This method traverses the annotations, interfaces, and superclasses
143+
* of the specified {@code clazz} if no annotation can be found on the given
144+
* class itself.
137145
* <p>This method explicitly handles class-level annotations which are not
138146
* declared as {@linkplain java.lang.annotation.Inherited inherited} <em>as
139147
* well as meta-annotations</em>.
@@ -143,14 +151,12 @@ private static <T extends Annotation> AnnotationDescriptor<T> findAnnotationDesc
143151
* the given class and return a corresponding {@code UntypedAnnotationDescriptor}
144152
* if found.
145153
* <li>Recursively search through all annotations that the given class declares.
154+
* <li>Recursively search through all interfaces implemented by the given class.
146155
* <li>Recursively search through the superclass hierarchy of the given class.
147156
* </ol>
148157
* <p>In this context, the term <em>recursively</em> means that the search
149-
* process continues by returning to step #1 with the current annotation or
150-
* superclass as the class to look for annotations on.
151-
* <p>If the supplied {@code clazz} is an interface, only the interface
152-
* itself will be checked; the inheritance hierarchy for interfaces will not
153-
* be traversed.
158+
* process continues by returning to step #1 with the current annotation,
159+
* interface, or superclass as the class to look for annotations on.
154160
* @param clazz the class to look for annotations on
155161
* @param annotationTypes the types of annotations to look for
156162
* @return the corresponding annotation descriptor if one of the annotations
@@ -203,6 +209,15 @@ private static UntypedAnnotationDescriptor findAnnotationDescriptorForTypes(Clas
203209
}
204210
}
205211

212+
// Declared on interface?
213+
for (Class<?> ifc : clazz.getInterfaces()) {
214+
UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(ifc, visited, annotationTypes);
215+
if (descriptor != null) {
216+
return new UntypedAnnotationDescriptor(clazz, descriptor.getDeclaringClass(),
217+
descriptor.getComposedAnnotation(), descriptor.getAnnotation());
218+
}
219+
}
220+
206221
// Declared on a superclass?
207222
return findAnnotationDescriptorForTypes(clazz.getSuperclass(), visited, annotationTypes);
208223
}

0 commit comments

Comments
 (0)