Allow @ContextConfiguration to be omitted

Prior to this commit, the @ContextConfiguration annotation was required
to be present even if default XML files, Groovy scripts, or
@Configuration classes were detected; however, in such cases the
@ContextConfiguration was typically declared empty and therefore
seemingly unnecessary boilerplate.

This commit permits @ContextConfiguration to be omitted whenever it can
be reasonably deduced. Consequently, integration tests such as the
following are now supported.

    @RunWith(SpringRunner.class)
    public class MyTest {

        @Autowired String myBean;

        @Test public void example() { /* ... */ }

        @Configuration
        static class Config {

            @Bean String myBean() {
                return "Hello";
            }
        }
    }

Issue: SPR-13955
This commit is contained in:
Phillip Webb
2016-02-28 23:05:00 -08:00
committed by Sam Brannen
parent 0a56667093
commit 2244461778
8 changed files with 221 additions and 94 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.
@@ -34,6 +34,7 @@ import org.springframework.util.StringUtils;
* attributes declared via {@link ContextConfiguration @ContextConfiguration}.
*
* @author Sam Brannen
* @author Phillip Webb
* @since 3.1
* @see ContextConfiguration
* @see SmartContextLoader#processContextConfiguration(ContextConfigurationAttributes)
@@ -43,6 +44,10 @@ public class ContextConfigurationAttributes {
private static final Log logger = LogFactory.getLog(ContextConfigurationAttributes.class);
private static final String[] EMPTY_LOCATIONS = new String[0];
private static final Class<?>[] EMPTY_CLASSES = new Class<?>[0];
private final Class<?> declaringClass;
private Class<?>[] classes;
@@ -60,6 +65,18 @@ public class ContextConfigurationAttributes {
private final Class<? extends ContextLoader> contextLoaderClass;
/**
* Construct a new {@link ContextConfigurationAttributes} instance with default
* values.
* @param declaringClass the test class that declared {@code @ContextConfiguration},
* either explicitly or implicitly
* @since 4.3
*/
@SuppressWarnings("unchecked")
public ContextConfigurationAttributes(Class<?> declaringClass) {
this(declaringClass, EMPTY_LOCATIONS, EMPTY_CLASSES, false, (Class[]) EMPTY_CLASSES, true, ContextLoader.class);
}
/**
* Construct a new {@link ContextConfigurationAttributes} instance for the
* supplied {@link ContextConfiguration @ContextConfiguration} annotation and
@@ -158,7 +175,8 @@ public class ContextConfigurationAttributes {
/**
* Get the {@linkplain Class class} that declared the
* {@link ContextConfiguration @ContextConfiguration} annotation.
* {@link ContextConfiguration @ContextConfiguration} annotation, either explicitly
* or implicitly.
* @return the declaring class (never {@code null})
*/
public Class<?> getDeclaringClass() {

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.
@@ -29,7 +29,6 @@ import org.springframework.test.context.ContextLoader;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.SmartContextLoader;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* {@code AbstractDelegatingSmartContextLoader} serves as an abstract base class
@@ -202,15 +201,6 @@ public abstract class AbstractDelegatingSmartContextLoader implements SmartConte
name(getAnnotationConfigLoader()), configAttributes));
}
// If neither loader detected defaults and no initializers were declared,
// throw an exception.
if (!configAttributes.hasResources() && ObjectUtils.isEmpty(configAttributes.getInitializers())) {
throw new IllegalStateException(String.format(
"Neither %s nor %s was able to detect defaults, and no ApplicationContextInitializers "
+ "were declared for context configuration %s", name(getXmlLoader()),
name(getAnnotationConfigLoader()), configAttributes));
}
if (configAttributes.hasLocations() && configAttributes.hasClasses()) {
String message = String.format(
"Configuration error: both default locations AND default configuration classes "

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.context.support;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
@@ -30,8 +31,6 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.io.support.SpringFactoriesLoader;
@@ -71,6 +70,7 @@ import org.springframework.util.StringUtils;
*
* @author Sam Brannen
* @author Juergen Hoeller
* @author Phillip Webb
* @since 4.1
*/
public abstract class AbstractTestContextBootstrapper implements TestContextBootstrapper {
@@ -272,13 +272,8 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = getCacheAwareContextLoaderDelegate();
if (MetaAnnotationUtils.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);
ContextHierarchy.class) == null) {
return buildDefaultMergedContextConfiguration(testClass, cacheAwareContextLoaderDelegate);
}
if (AnnotationUtils.findAnnotation(testClass, ContextHierarchy.class) != null) {
@@ -297,7 +292,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
Class<?> declaringClass = reversedList.get(0).getDeclaringClass();
mergedConfig = buildMergedContextConfiguration(declaringClass, reversedList, parentConfig,
cacheAwareContextLoaderDelegate);
cacheAwareContextLoaderDelegate, true);
parentConfig = mergedConfig;
}
@@ -307,10 +302,29 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
else {
return buildMergedContextConfiguration(testClass,
ContextLoaderUtils.resolveContextConfigurationAttributes(testClass), null,
cacheAwareContextLoaderDelegate);
cacheAwareContextLoaderDelegate, true);
}
}
/**
* @since 4.3
*/
private MergedContextConfiguration buildDefaultMergedContextConfiguration(Class<?> testClass,
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {
List<ContextConfigurationAttributes> defaultConfigAttributesList
= Collections.singletonList(new ContextConfigurationAttributes(testClass));
ContextLoader contextLoader = resolveContextLoader(testClass, defaultConfigAttributesList);
if (logger.isInfoEnabled()) {
logger.info(String.format(
"Neither @ContextConfiguration nor @ContextHierarchy found for test class [%s], using %s",
testClass.getName(), contextLoader.getClass().getSimpleName()));
}
return buildMergedContextConfiguration(testClass, defaultConfigAttributesList, null,
cacheAwareContextLoaderDelegate, false);
}
/**
* Build the {@link MergedContextConfiguration merged context configuration}
* for the supplied {@link Class testClass}, context configuration attributes,
@@ -324,6 +338,9 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
* context in a context hierarchy, or {@code null} if there is no parent
* @param cacheAwareContextLoaderDelegate the cache-aware context loader delegate to
* be passed to the {@code MergedContextConfiguration} constructor
* @param requireLocationsClassesOrInitializers whether locations, classes, or
* initializers are required; typically {@code true} but may be set to {@code false}
* if the configured loader supports empty configuration
* @return the merged context configuration
* @see #resolveContextLoader
* @see ContextLoaderUtils#resolveContextConfigurationAttributes
@@ -335,11 +352,15 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
*/
private MergedContextConfiguration buildMergedContextConfiguration(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributesList, MergedContextConfiguration parentConfig,
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate,
boolean requireLocationsClassesOrInitializers) {
Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be null or empty");
ContextLoader contextLoader = resolveContextLoader(testClass, configAttributesList);
List<String> locationsList = new ArrayList<String>();
List<Class<?>> classesList = new ArrayList<Class<?>>();
List<String> locations = new ArrayList<String>();
List<Class<?>> classes = new ArrayList<Class<?>>();
List<Class<?>> initializers = new ArrayList<Class<?>>();
for (ContextConfigurationAttributes configAttributes : configAttributesList) {
if (logger.isTraceEnabled()) {
@@ -349,34 +370,53 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
if (contextLoader instanceof SmartContextLoader) {
SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader;
smartContextLoader.processContextConfiguration(configAttributes);
locationsList.addAll(0, Arrays.asList(configAttributes.getLocations()));
classesList.addAll(0, Arrays.asList(configAttributes.getClasses()));
locations.addAll(0, Arrays.asList(configAttributes.getLocations()));
classes.addAll(0, Arrays.asList(configAttributes.getClasses()));
}
else {
String[] processedLocations = contextLoader.processLocations(configAttributes.getDeclaringClass(),
configAttributes.getLocations());
locationsList.addAll(0, Arrays.asList(processedLocations));
String[] processedLocations = contextLoader.processLocations(
configAttributes.getDeclaringClass(), configAttributes.getLocations());
locations.addAll(0, Arrays.asList(processedLocations));
// Legacy ContextLoaders don't know how to process classes
}
initializers.addAll(0, Arrays.asList(configAttributes.getInitializers()));
if (!configAttributes.isInheritLocations()) {
break;
}
}
String[] locations = StringUtils.toStringArray(locationsList);
Class<?>[] classes = ClassUtils.toClassArray(classesList);
Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses = //
ApplicationContextInitializerUtils.resolveInitializerClasses(configAttributesList);
String[] activeProfiles = ActiveProfilesUtils.resolveActiveProfiles(testClass);
MergedTestPropertySources mergedTestPropertySources = TestPropertySourceUtils.buildMergedTestPropertySources(testClass);
if (requireLocationsClassesOrInitializers && areAllEmpty(locations, classes, initializers)) {
throw new IllegalStateException(String.format(
"%s was unable to detect defaults, and no ApplicationContextInitializers "
+ "were declared for context configuration attributes %s",
contextLoader.getClass().getSimpleName(), configAttributesList));
}
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(testClass, locations, classes,
initializerClasses, activeProfiles, mergedTestPropertySources.getLocations(),
mergedTestPropertySources.getProperties(), contextLoader, cacheAwareContextLoaderDelegate, parentConfig);
MergedTestPropertySources mergedTestPropertySources = TestPropertySourceUtils.buildMergedTestPropertySources(testClass);
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(testClass,
StringUtils.toStringArray(locations),
ClassUtils.toClassArray(classes),
ApplicationContextInitializerUtils.resolveInitializerClasses(configAttributesList),
ActiveProfilesUtils.resolveActiveProfiles(testClass),
mergedTestPropertySources.getLocations(),
mergedTestPropertySources.getProperties(),
contextLoader, cacheAwareContextLoaderDelegate, parentConfig);
return processMergedContextConfiguration(mergedConfig);
}
/**
* @since 4.3
*/
private boolean areAllEmpty(Collection<?>... collections) {
for (Collection<?> collection : collections) {
if (!collection.isEmpty()) {
return false;
}
}
return true;
}
/**
* Resolve the {@link ContextLoader} {@linkplain Class class} to use for the
* supplied list of {@link ContextConfigurationAttributes} and then instantiate
@@ -389,7 +429,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
* @param testClass the test class for which the {@code ContextLoader} should be
* resolved; must not be {@code null}
* @param configAttributesList the list of configuration attributes to process; must
* not be {@code null} or <em>empty</em>; must be ordered <em>bottom-up</em>
* not be {@code null}; must be ordered <em>bottom-up</em>
* (i.e., as if we were traversing up the class hierarchy)
* @return the resolved {@code ContextLoader} for the supplied {@code testClass}
* (never {@code null})
@@ -400,7 +440,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
List<ContextConfigurationAttributes> configAttributesList) {
Assert.notNull(testClass, "Class must not be null");
Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty");
Assert.notNull(configAttributesList, "ContextConfigurationAttributes list must not be null");
Class<? extends ContextLoader> contextLoaderClass = resolveExplicitContextLoaderClass(configAttributesList);
if (contextLoaderClass == null) {
@@ -429,7 +469,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
* step #1.</li>
* </ol>
* @param configAttributesList the list of configuration attributes to process;
* must not be {@code null} or <em>empty</em>; must be ordered <em>bottom-up</em>
* must not be {@code null}; must be ordered <em>bottom-up</em>
* (i.e., as if we were traversing up the class hierarchy)
* @return the {@code ContextLoader} class to use for the supplied configuration
* attributes, or {@code null} if no explicit loader is found
@@ -439,7 +479,8 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
protected Class<? extends ContextLoader> resolveExplicitContextLoaderClass(
List<ContextConfigurationAttributes> configAttributesList) {
Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty");
Assert.notNull(configAttributesList, "ContextConfigurationAttributes list must not be null");
for (ContextConfigurationAttributes configAttributes : configAttributesList) {
if (logger.isTraceEnabled()) {
logger.trace(String.format("Resolving ContextLoader for context configuration attributes %s",