Skip to content

Commit ae37c63

Browse files
committed
Support @ContextConfiguration at method level
Prior Spring TestContext Framework had not supported @ContextConfiguration annotation on the method-level. This commit enables @ContextConfiguration and @ContextHierarchy to be the method-level annotations. Now TCF will emit fully configured throw-away TestContext for the particular test method. Issue: SPR-12031
1 parent 4c005e6 commit ae37c63

21 files changed

+3146
-76
lines changed

spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ private static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A>
731731
* @see Class#isAnnotationPresent(Class)
732732
* @see Class#getDeclaredAnnotations()
733733
* @see #findAnnotationDeclaringClassForTypes(List, Class)
734-
* @see #isAnnotationDeclaredLocally(Class, Class)
734+
* @see #isAnnotationDeclaredLocally(Class, AnnotatedElement)
735735
*/
736736
public static Class<?> findAnnotationDeclaringClass(Class<? extends Annotation> annotationType, Class<?> clazz) {
737737
Assert.notNull(annotationType, "Annotation type must not be null");
@@ -766,7 +766,7 @@ public static Class<?> findAnnotationDeclaringClass(Class<? extends Annotation>
766766
* @see Class#isAnnotationPresent(Class)
767767
* @see Class#getDeclaredAnnotations()
768768
* @see #findAnnotationDeclaringClass(Class, Class)
769-
* @see #isAnnotationDeclaredLocally(Class, Class)
769+
* @see #isAnnotationDeclaredLocally(Class, AnnotatedElement)
770770
*/
771771
public static Class<?> findAnnotationDeclaringClassForTypes(List<Class<? extends Annotation>> annotationTypes, Class<?> clazz) {
772772
Assert.notEmpty(annotationTypes, "List of annotation types must not be empty");
@@ -784,33 +784,34 @@ public static Class<?> findAnnotationDeclaringClassForTypes(List<Class<? extends
784784
/**
785785
* Determine whether an annotation of the specified {@code annotationType}
786786
* is declared locally (i.e., <em>directly present</em>) on the supplied
787-
* {@code clazz}.
788-
* <p>The supplied {@link Class} may represent any type.
787+
* {@code annotatedElement}.
788+
* <p>The supplied {@link AnnotatedElement} may represents any annotated element.
789789
* <p>Meta-annotations will <em>not</em> be searched.
790790
* <p>Note: This method does <strong>not</strong> determine if the annotation
791791
* is {@linkplain java.lang.annotation.Inherited inherited}. For greater
792792
* clarity regarding inherited annotations, consider using
793793
* {@link #isAnnotationInherited(Class, Class)} instead.
794794
* @param annotationType the annotation type to look for
795-
* @param clazz the class to check for the annotation on
795+
* @param annotatedElement the element to check for the annotation on
796796
* @return {@code true} if an annotation of the specified {@code annotationType}
797797
* is <em>directly present</em>
798798
* @see java.lang.Class#getDeclaredAnnotations()
799799
* @see java.lang.Class#getDeclaredAnnotation(Class)
800800
* @see #isAnnotationInherited(Class, Class)
801801
*/
802-
public static boolean isAnnotationDeclaredLocally(Class<? extends Annotation> annotationType, Class<?> clazz) {
802+
public static boolean isAnnotationDeclaredLocally(Class<? extends Annotation> annotationType,
803+
AnnotatedElement annotatedElement) {
803804
Assert.notNull(annotationType, "Annotation type must not be null");
804-
Assert.notNull(clazz, "Class must not be null");
805+
Assert.notNull(annotatedElement, "Annotated element must not be null");
805806
try {
806-
for (Annotation ann : clazz.getDeclaredAnnotations()) {
807+
for (Annotation ann : annotatedElement.getDeclaredAnnotations()) {
807808
if (ann.annotationType() == annotationType) {
808809
return true;
809810
}
810811
}
811812
}
812813
catch (Throwable ex) {
813-
handleIntrospectionFailure(clazz, ex);
814+
handleIntrospectionFailure(annotatedElement, ex);
814815
}
815816
return false;
816817
}
@@ -832,7 +833,7 @@ public static boolean isAnnotationDeclaredLocally(Class<? extends Annotation> an
832833
* @return {@code true} if an annotation of the specified {@code annotationType}
833834
* is <em>present</em> and <em>inherited</em>
834835
* @see Class#isAnnotationPresent(Class)
835-
* @see #isAnnotationDeclaredLocally(Class, Class)
836+
* @see #isAnnotationDeclaredLocally(Class, AnnotatedElement)
836837
*/
837838
public static boolean isAnnotationInherited(Class<? extends Annotation> annotationType, Class<?> clazz) {
838839
Assert.notNull(annotationType, "Annotation type must not be null");

spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,14 @@ public void isAnnotationDeclaredLocallyForAllScenarios() throws Exception {
407407
assertFalse(isAnnotationDeclaredLocally(Order.class, SubNonInheritedAnnotationInterface.class));
408408
assertTrue(isAnnotationDeclaredLocally(Order.class, NonInheritedAnnotationClass.class));
409409
assertFalse(isAnnotationDeclaredLocally(Order.class, SubNonInheritedAnnotationClass.class));
410+
411+
// inherited method-level annotation; note: @Transactional is inherited
412+
assertTrue(isAnnotationDeclaredLocally(Transactional.class, InheritedAnnotationClass.class.getMethod("something")));
413+
assertFalse(isAnnotationDeclaredLocally(Transactional.class, SubInheritedAnnotationClass.class.getMethod("something")));
414+
415+
// non-inherited method-level annotation; note: @Order is not inherited
416+
assertTrue(isAnnotationDeclaredLocally(Order.class, NonInheritedAnnotationInterface.class.getMethod("something")));
417+
assertFalse(isAnnotationDeclaredLocally(Order.class, SubNonInheritedAnnotationInterface.class.getMethod("something")));
410418
}
411419

412420
@Test
@@ -1719,9 +1727,13 @@ public interface SubSubInheritedAnnotationInterface extends SubInheritedAnnotati
17191727

17201728
@Order
17211729
public interface NonInheritedAnnotationInterface {
1730+
@Order
1731+
void something();
17221732
}
17231733

17241734
public interface SubNonInheritedAnnotationInterface extends NonInheritedAnnotationInterface {
1735+
@Override
1736+
void something();
17251737
}
17261738

17271739
public interface SubSubNonInheritedAnnotationInterface extends SubNonInheritedAnnotationInterface {
@@ -1735,9 +1747,17 @@ public interface NonAnnotatedInterface {
17351747

17361748
@Transactional
17371749
public static class InheritedAnnotationClass {
1750+
@Transactional
1751+
public void something() {
1752+
// for test purposes
1753+
}
17381754
}
17391755

17401756
public static class SubInheritedAnnotationClass extends InheritedAnnotationClass {
1757+
@Override
1758+
public void something() {
1759+
// for test purposes
1760+
}
17411761
}
17421762

17431763
@Order

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.test.context;
1818

1919
import java.lang.reflect.Constructor;
20+
import java.lang.reflect.Method;
2021
import java.util.Set;
2122

2223
import org.apache.commons.logging.Log;
@@ -50,6 +51,9 @@ abstract class BootstrapUtils {
5051
private static final String DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME =
5152
"org.springframework.test.context.support.DefaultTestContextBootstrapper";
5253

54+
private static final String DEFAULT_TEST_METHOD_CONTEXT_BOOTSTRAPPER_CLASS_NAME =
55+
"org.springframework.test.context.support.DefaultTestMethodContextBootstrapper";
56+
5357
private static final String DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME =
5458
"org.springframework.test.context.web.WebTestContextBootstrapper";
5559

@@ -147,6 +151,42 @@ static TestContextBootstrapper resolveTestContextBootstrapper(BootstrapContext b
147151
}
148152
}
149153

154+
/**
155+
* Resolve the {@link TestContextBootstrapper} type for the supplied test method.
156+
* <p>Will use
157+
* {@link org.springframework.test.context.support.DefaultTestMethodContextBootstrapper
158+
* DefaultTestMethodContextBootstrapper}.
159+
* @param testMethod the method for which to create context bootstrapper
160+
* @return a fully configured {@code TestContextBootstrapper} for particular method
161+
*/
162+
@SuppressWarnings("unchecked")
163+
static TestContextBootstrapper resolveTestContextBootstrapper(Method testMethod) {
164+
final BootstrapContext bootstrapContext = createBootstrapContext(testMethod.getDeclaringClass());
165+
Class<?> testClass = bootstrapContext.getTestClass();
166+
167+
Class<?> clazz = null;
168+
try {
169+
clazz = ClassUtils.forName(DEFAULT_TEST_METHOD_CONTEXT_BOOTSTRAPPER_CLASS_NAME,
170+
BootstrapUtils.class.getClassLoader());
171+
if (logger.isDebugEnabled()) {
172+
logger.debug(String.format("Instantiating TestContextBootstrapper for test class [%s] from class [%s]",
173+
testClass.getName(), clazz.getName()));
174+
}
175+
TestContextBootstrapper testContextBootstrapper = BeanUtils.instantiateClass(
176+
(Constructor<? extends TestContextBootstrapper>) clazz.getConstructor(Method.class), testMethod);
177+
testContextBootstrapper.setBootstrapContext(bootstrapContext);
178+
return testContextBootstrapper;
179+
}
180+
catch (IllegalStateException ex) {
181+
throw ex;
182+
}
183+
catch (Throwable ex) {
184+
throw new IllegalStateException("Could not load TestContextBootstrapper [" + clazz +
185+
"]. Specify @BootstrapWith's 'value' attribute or make the default bootstrapper class available.",
186+
ex);
187+
}
188+
}
189+
150190
private static Class<?> resolveExplicitTestContextBootstrapper(Class<?> testClass) {
151191
Set<BootstrapWith> annotations = AnnotatedElementUtils.findAllMergedAnnotations(testClass, BootstrapWith.class);
152192
if (annotations.size() < 1) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
* @see MergedContextConfiguration
8484
* @see org.springframework.context.ApplicationContext
8585
*/
86-
@Target(ElementType.TYPE)
86+
@Target({ElementType.TYPE, ElementType.METHOD})
8787
@Retention(RetentionPolicy.RUNTIME)
8888
@Documented
8989
@Inherited

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.test.context;
1818

19+
import java.lang.reflect.Method;
1920
import java.util.Arrays;
2021

2122
import org.apache.commons.logging.Log;
@@ -51,6 +52,8 @@ public class ContextConfigurationAttributes {
5152

5253
private final Class<?> declaringClass;
5354

55+
private final Method declaringMethod;
56+
5457
private Class<?>[] classes;
5558

5659
private String[] locations;
@@ -149,20 +152,29 @@ public ContextConfigurationAttributes(
149152
Class<?> declaringClass, String[] locations, Class<?>[] classes, boolean inheritLocations,
150153
Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers,
151154
boolean inheritInitializers, String name, Class<? extends ContextLoader> contextLoaderClass) {
155+
this(declaringClass, null, locations, classes, inheritLocations, initializers, inheritInitializers,
156+
name, contextLoaderClass);
157+
}
158+
159+
public ContextConfigurationAttributes(
160+
Class<?> declaringClass, Method declaringMethod, String[] locations, Class<?>[] classes, boolean inheritLocations,
161+
Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers,
162+
boolean inheritInitializers, String name, Class<? extends ContextLoader> contextLoaderClass) {
152163

153164
Assert.notNull(declaringClass, "declaringClass must not be null");
154165
Assert.notNull(contextLoaderClass, "contextLoaderClass must not be null");
155166

156167
if (!ObjectUtils.isEmpty(locations) && !ObjectUtils.isEmpty(classes) && logger.isDebugEnabled()) {
157168
logger.debug(String.format(
158169
"Test class [%s] has been configured with @ContextConfiguration's 'locations' (or 'value') %s " +
159-
"and 'classes' %s attributes. Most SmartContextLoader implementations support " +
160-
"only one declaration of resources per @ContextConfiguration annotation.",
170+
"and 'classes' %s attributes. Most SmartContextLoader implementations support " +
171+
"only one declaration of resources per @ContextConfiguration annotation.",
161172
declaringClass.getName(), ObjectUtils.nullSafeToString(locations),
162173
ObjectUtils.nullSafeToString(classes)));
163174
}
164175

165176
this.declaringClass = declaringClass;
177+
this.declaringMethod = declaringMethod;
166178
this.locations = locations;
167179
this.classes = classes;
168180
this.inheritLocations = inheritLocations;
@@ -183,6 +195,16 @@ public Class<?> getDeclaringClass() {
183195
return this.declaringClass;
184196
}
185197

198+
/**
199+
* Get the {@linkplain Method method} that declared the
200+
* {@link ContextConfiguration @ContextConfiguration} annotation, either explicitly
201+
* or implicitly.
202+
* @return the declaring method (may be {@code null})
203+
*/
204+
public Method getDeclaringMethod() {
205+
return this.declaringMethod;
206+
}
207+
186208
/**
187209
* Set the <em>processed</em> annotated classes, effectively overriding the
188210
* original value declared via {@link ContextConfiguration @ContextConfiguration}.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@
139139
* @see ContextConfiguration
140140
* @see org.springframework.context.ApplicationContext
141141
*/
142-
@Target(ElementType.TYPE)
142+
@Target({ElementType.TYPE, ElementType.METHOD})
143143
@Retention(RetentionPolicy.RUNTIME)
144144
@Documented
145145
@Inherited

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import org.apache.commons.logging.Log;
2626
import org.apache.commons.logging.LogFactory;
2727

28+
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
29+
import org.springframework.test.util.MetaAnnotationUtils;
2830
import org.springframework.util.Assert;
2931
import org.springframework.util.ClassUtils;
3032
import org.springframework.util.ReflectionUtils;
@@ -282,10 +284,11 @@ public void prepareTestInstance(Object testInstance) throws Exception {
282284
public void beforeTestMethod(Object testInstance, Method testMethod) throws Exception {
283285
String callbackName = "beforeTestMethod";
284286
prepareForBeforeCallback(callbackName, testInstance, testMethod);
287+
TestContext preparedContext = prepareTestContextBeforeMethod(getTestContext());
285288

286289
for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) {
287290
try {
288-
testExecutionListener.beforeTestMethod(getTestContext());
291+
testExecutionListener.beforeTestMethod(preparedContext);
289292
}
290293
catch (Throwable ex) {
291294
handleBeforeException(ex, callbackName, testExecutionListener, testInstance, testMethod);
@@ -294,6 +297,46 @@ public void beforeTestMethod(Object testInstance, Method testMethod) throws Exce
294297
}
295298

296299
/**
300+
* Prepares {@linkplain TestContext test context} in the sense
301+
* that if current test method annotated with context configuration
302+
* annotation e.g. {@link ContextConfiguration}, {@link ContextHierarchy}
303+
* then emits throw-away context, constructed specifically for current
304+
* test method, otherwise returns original test context without modifications.
305+
*
306+
* @param originalTestContext test class test context
307+
* @return throw-away context if current method has context annotation or
308+
* originalTestContext otherwise.
309+
*
310+
* @since 5.0
311+
*/
312+
private TestContext prepareTestContextBeforeMethod(TestContext originalTestContext) {
313+
Method testMethod = originalTestContext.getTestMethod();
314+
if (testMethod != null && MetaAnnotationUtils.findAnnotationDescriptorForTypes(testMethod,
315+
ContextConfiguration.class, ContextHierarchy.class) != null) {
316+
return createThrowAwayContext(testMethod, originalTestContext);
317+
}
318+
return originalTestContext;
319+
}
320+
321+
/**
322+
* Creates throw-away context for supplied {@linkplain Method testMethod}
323+
* and copies the state of original context in the created one.
324+
* Sets an attribute {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE}
325+
* of the created context to <code>true</code> to make reinjection.
326+
* @param testMethod test method annotated with context configuration annotation
327+
* @param originalContext original test context to copy state into created one
328+
* @return throw-away context for specified test method
329+
* @since 5.0
330+
*/
331+
private TestContext createThrowAwayContext(Method testMethod, TestContext originalContext) {
332+
TestContext throwAwayContext = BootstrapUtils.resolveTestContextBootstrapper(testMethod).buildTestContext();
333+
throwAwayContext.updateState(originalContext.getTestInstance(), testMethod, null);
334+
throwAwayContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE,
335+
Boolean.TRUE);
336+
return throwAwayContext;
337+
}
338+
339+
/**
297340
* Hook for pre-processing a test <em>immediately before</em> execution of
298341
* the {@linkplain java.lang.reflect.Method test method} in the supplied
299342
* {@linkplain TestContext test context} &mdash; for example, for timing

0 commit comments

Comments
 (0)