Skip to content

SPR-12031 Support @ContextConfiguration at method level #1255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,7 @@ private static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A>
* @see Class#isAnnotationPresent(Class)
* @see Class#getDeclaredAnnotations()
* @see #findAnnotationDeclaringClassForTypes(List, Class)
* @see #isAnnotationDeclaredLocally(Class, Class)
* @see #isAnnotationDeclaredLocally(Class, AnnotatedElement)
*/
public static Class<?> findAnnotationDeclaringClass(Class<? extends Annotation> annotationType, Class<?> clazz) {
Assert.notNull(annotationType, "Annotation type must not be null");
Expand Down Expand Up @@ -766,7 +766,7 @@ public static Class<?> findAnnotationDeclaringClass(Class<? extends Annotation>
* @see Class#isAnnotationPresent(Class)
* @see Class#getDeclaredAnnotations()
* @see #findAnnotationDeclaringClass(Class, Class)
* @see #isAnnotationDeclaredLocally(Class, Class)
* @see #isAnnotationDeclaredLocally(Class, AnnotatedElement)
*/
public static Class<?> findAnnotationDeclaringClassForTypes(List<Class<? extends Annotation>> annotationTypes, Class<?> clazz) {
Assert.notEmpty(annotationTypes, "List of annotation types must not be empty");
Expand All @@ -784,33 +784,34 @@ public static Class<?> findAnnotationDeclaringClassForTypes(List<Class<? extends
/**
* Determine whether an annotation of the specified {@code annotationType}
* is declared locally (i.e., <em>directly present</em>) on the supplied
* {@code clazz}.
* <p>The supplied {@link Class} may represent any type.
* {@code annotatedElement}.
* <p>The supplied {@link AnnotatedElement} may represents any annotated element.
* <p>Meta-annotations will <em>not</em> be searched.
* <p>Note: This method does <strong>not</strong> determine if the annotation
* is {@linkplain java.lang.annotation.Inherited inherited}. For greater
* clarity regarding inherited annotations, consider using
* {@link #isAnnotationInherited(Class, Class)} instead.
* @param annotationType the annotation type to look for
* @param clazz the class to check for the annotation on
* @param annotatedElement the element to check for the annotation on
* @return {@code true} if an annotation of the specified {@code annotationType}
* is <em>directly present</em>
* @see java.lang.Class#getDeclaredAnnotations()
* @see java.lang.Class#getDeclaredAnnotation(Class)
* @see #isAnnotationInherited(Class, Class)
*/
public static boolean isAnnotationDeclaredLocally(Class<? extends Annotation> annotationType, Class<?> clazz) {
public static boolean isAnnotationDeclaredLocally(Class<? extends Annotation> annotationType,
AnnotatedElement annotatedElement) {
Assert.notNull(annotationType, "Annotation type must not be null");
Assert.notNull(clazz, "Class must not be null");
Assert.notNull(annotatedElement, "Annotated element must not be null");
try {
for (Annotation ann : clazz.getDeclaredAnnotations()) {
for (Annotation ann : annotatedElement.getDeclaredAnnotations()) {
if (ann.annotationType() == annotationType) {
return true;
}
}
}
catch (Throwable ex) {
handleIntrospectionFailure(clazz, ex);
handleIntrospectionFailure(annotatedElement, ex);
}
return false;
}
Expand All @@ -832,7 +833,7 @@ public static boolean isAnnotationDeclaredLocally(Class<? extends Annotation> an
* @return {@code true} if an annotation of the specified {@code annotationType}
* is <em>present</em> and <em>inherited</em>
* @see Class#isAnnotationPresent(Class)
* @see #isAnnotationDeclaredLocally(Class, Class)
* @see #isAnnotationDeclaredLocally(Class, AnnotatedElement)
*/
public static boolean isAnnotationInherited(Class<? extends Annotation> annotationType, Class<?> clazz) {
Assert.notNull(annotationType, "Annotation type must not be null");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,14 @@ public void isAnnotationDeclaredLocallyForAllScenarios() throws Exception {
assertFalse(isAnnotationDeclaredLocally(Order.class, SubNonInheritedAnnotationInterface.class));
assertTrue(isAnnotationDeclaredLocally(Order.class, NonInheritedAnnotationClass.class));
assertFalse(isAnnotationDeclaredLocally(Order.class, SubNonInheritedAnnotationClass.class));

// inherited method-level annotation; note: @Transactional is inherited
assertTrue(isAnnotationDeclaredLocally(Transactional.class, InheritedAnnotationClass.class.getMethod("something")));
assertFalse(isAnnotationDeclaredLocally(Transactional.class, SubInheritedAnnotationClass.class.getMethod("something")));

// non-inherited method-level annotation; note: @Order is not inherited
assertTrue(isAnnotationDeclaredLocally(Order.class, NonInheritedAnnotationInterface.class.getMethod("something")));
assertFalse(isAnnotationDeclaredLocally(Order.class, SubNonInheritedAnnotationInterface.class.getMethod("something")));
}

@Test
Expand Down Expand Up @@ -1719,9 +1727,13 @@ public interface SubSubInheritedAnnotationInterface extends SubInheritedAnnotati

@Order
public interface NonInheritedAnnotationInterface {
@Order
void something();
}

public interface SubNonInheritedAnnotationInterface extends NonInheritedAnnotationInterface {
@Override
void something();
}

public interface SubSubNonInheritedAnnotationInterface extends SubNonInheritedAnnotationInterface {
Expand All @@ -1735,9 +1747,17 @@ public interface NonAnnotatedInterface {

@Transactional
public static class InheritedAnnotationClass {
@Transactional
public void something() {
// for test purposes
}
}

public static class SubInheritedAnnotationClass extends InheritedAnnotationClass {
@Override
public void something() {
// for test purposes
}
}

@Order
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.springframework.test.context;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Set;

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

private static final String DEFAULT_TEST_METHOD_CONTEXT_BOOTSTRAPPER_CLASS_NAME =
"org.springframework.test.context.support.DefaultTestMethodContextBootstrapper";

private static final String DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME =
"org.springframework.test.context.web.WebTestContextBootstrapper";

Expand Down Expand Up @@ -147,6 +151,42 @@ static TestContextBootstrapper resolveTestContextBootstrapper(BootstrapContext b
}
}

/**
* Resolve the {@link TestContextBootstrapper} type for the supplied test method.
* <p>Will use
* {@link org.springframework.test.context.support.DefaultTestMethodContextBootstrapper
* DefaultTestMethodContextBootstrapper}.
* @param testMethod the method for which to create context bootstrapper
* @return a fully configured {@code TestContextBootstrapper} for particular method
*/
@SuppressWarnings("unchecked")
static TestContextBootstrapper resolveTestContextBootstrapper(Method testMethod) {
final BootstrapContext bootstrapContext = createBootstrapContext(testMethod.getDeclaringClass());
Class<?> testClass = bootstrapContext.getTestClass();

Class<?> clazz = null;
try {
clazz = ClassUtils.forName(DEFAULT_TEST_METHOD_CONTEXT_BOOTSTRAPPER_CLASS_NAME,
BootstrapUtils.class.getClassLoader());
if (logger.isDebugEnabled()) {
logger.debug(String.format("Instantiating TestContextBootstrapper for test class [%s] from class [%s]",
testClass.getName(), clazz.getName()));
}
TestContextBootstrapper testContextBootstrapper = BeanUtils.instantiateClass(
(Constructor<? extends TestContextBootstrapper>) clazz.getConstructor(Method.class), testMethod);
testContextBootstrapper.setBootstrapContext(bootstrapContext);
return testContextBootstrapper;
}
catch (IllegalStateException ex) {
throw ex;
}
catch (Throwable ex) {
throw new IllegalStateException("Could not load TestContextBootstrapper [" + clazz +
"]. Specify @BootstrapWith's 'value' attribute or make the default bootstrapper class available.",
ex);
}
}

private static Class<?> resolveExplicitTestContextBootstrapper(Class<?> testClass) {
Set<BootstrapWith> annotations = AnnotatedElementUtils.findAllMergedAnnotations(testClass, BootstrapWith.class);
if (annotations.size() < 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
* @see MergedContextConfiguration
* @see org.springframework.context.ApplicationContext
*/
@Target(ElementType.TYPE)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.springframework.test.context;

import java.lang.reflect.Method;
import java.util.Arrays;

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

private final Class<?> declaringClass;

private final Method declaringMethod;

private Class<?>[] classes;

private String[] locations;
Expand Down Expand Up @@ -149,20 +152,29 @@ public ContextConfigurationAttributes(
Class<?> declaringClass, String[] locations, Class<?>[] classes, boolean inheritLocations,
Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers,
boolean inheritInitializers, String name, Class<? extends ContextLoader> contextLoaderClass) {
this(declaringClass, null, locations, classes, inheritLocations, initializers, inheritInitializers,
name, contextLoaderClass);
}

public ContextConfigurationAttributes(
Class<?> declaringClass, Method declaringMethod, String[] locations, Class<?>[] classes, boolean inheritLocations,
Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers,
boolean inheritInitializers, String name, Class<? extends ContextLoader> contextLoaderClass) {

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

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

this.declaringClass = declaringClass;
this.declaringMethod = declaringMethod;
this.locations = locations;
this.classes = classes;
this.inheritLocations = inheritLocations;
Expand All @@ -183,6 +195,16 @@ public Class<?> getDeclaringClass() {
return this.declaringClass;
}

/**
* Get the {@linkplain Method method} that declared the
* {@link ContextConfiguration @ContextConfiguration} annotation, either explicitly
* or implicitly.
* @return the declaring method (may be {@code null})
*/
public Method getDeclaringMethod() {
return this.declaringMethod;
}

/**
* Set the <em>processed</em> annotated classes, effectively overriding the
* original value declared via {@link ContextConfiguration @ContextConfiguration}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@
* @see ContextConfiguration
* @see org.springframework.context.ApplicationContext
*/
@Target(ElementType.TYPE)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.util.MetaAnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
Expand Down Expand Up @@ -282,10 +284,11 @@ public void prepareTestInstance(Object testInstance) throws Exception {
public void beforeTestMethod(Object testInstance, Method testMethod) throws Exception {
String callbackName = "beforeTestMethod";
prepareForBeforeCallback(callbackName, testInstance, testMethod);
TestContext preparedContext = prepareTestContextBeforeMethod(getTestContext());

for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) {
try {
testExecutionListener.beforeTestMethod(getTestContext());
testExecutionListener.beforeTestMethod(preparedContext);
}
catch (Throwable ex) {
handleBeforeException(ex, callbackName, testExecutionListener, testInstance, testMethod);
Expand All @@ -294,6 +297,46 @@ public void beforeTestMethod(Object testInstance, Method testMethod) throws Exce
}

/**
* Prepares {@linkplain TestContext test context} in the sense
* that if current test method annotated with context configuration
* annotation e.g. {@link ContextConfiguration}, {@link ContextHierarchy}
* then emits throw-away context, constructed specifically for current
* test method, otherwise returns original test context without modifications.
*
* @param originalTestContext test class test context
* @return throw-away context if current method has context annotation or
* originalTestContext otherwise.
*
* @since 5.0
*/
private TestContext prepareTestContextBeforeMethod(TestContext originalTestContext) {
Method testMethod = originalTestContext.getTestMethod();
if (testMethod != null && MetaAnnotationUtils.findAnnotationDescriptorForTypes(testMethod,
ContextConfiguration.class, ContextHierarchy.class) != null) {
return createThrowAwayContext(testMethod, originalTestContext);
}
return originalTestContext;
}

/**
* Creates throw-away context for supplied {@linkplain Method testMethod}
* and copies the state of original context in the created one.
* Sets an attribute {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE}
* of the created context to <code>true</code> to make reinjection.
* @param testMethod test method annotated with context configuration annotation
* @param originalContext original test context to copy state into created one
* @return throw-away context for specified test method
* @since 5.0
*/
private TestContext createThrowAwayContext(Method testMethod, TestContext originalContext) {
TestContext throwAwayContext = BootstrapUtils.resolveTestContextBootstrapper(testMethod).buildTestContext();
throwAwayContext.updateState(originalContext.getTestInstance(), testMethod, null);
throwAwayContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE,
Boolean.TRUE);
return throwAwayContext;
}

/**
* Hook for pre-processing a test <em>immediately before</em> execution of
* the {@linkplain java.lang.reflect.Method test method} in the supplied
* {@linkplain TestContext test context} &mdash; for example, for timing
Expand Down
Loading