org.springframework.test.context.support.AbstractTestContextBootstrapper Maven / Gradle / Ivy
Show all versions of spring-test Show documentation
/*
* Copyright 2002-2018 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.BeanUtils;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.lang.Nullable;
import org.springframework.test.context.BootstrapContext;
import org.springframework.test.context.CacheAwareContextLoaderDelegate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
import org.springframework.test.context.ContextHierarchy;
import org.springframework.test.context.ContextLoader;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.SmartContextLoader;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestContextBootstrapper;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.TestExecutionListeners.MergeMode;
import org.springframework.test.util.MetaAnnotationUtils;
import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Abstract implementation of the {@link TestContextBootstrapper} interface which
* provides most of the behavior required by a bootstrapper.
*
* Concrete subclasses typically will only need to provide implementations for
* the following methods:
*
* - {@link #getDefaultContextLoaderClass}
*
- {@link #processMergedContextConfiguration}
*
*
* To plug in custom
* {@link org.springframework.test.context.cache.ContextCache ContextCache}
* support, override {@link #getCacheAwareContextLoaderDelegate()}.
*
* @author Sam Brannen
* @author Juergen Hoeller
* @author Phillip Webb
* @since 4.1
*/
public abstract class AbstractTestContextBootstrapper implements TestContextBootstrapper {
private final Log logger = LogFactory.getLog(getClass());
@Nullable
private BootstrapContext bootstrapContext;
@Override
public void setBootstrapContext(BootstrapContext bootstrapContext) {
this.bootstrapContext = bootstrapContext;
}
@Override
public BootstrapContext getBootstrapContext() {
Assert.state(this.bootstrapContext != null, "No BootstrapContext set");
return this.bootstrapContext;
}
/**
* Build a new {@link DefaultTestContext} using the {@linkplain Class test class}
* in the {@link BootstrapContext} associated with this bootstrapper and
* by delegating to {@link #buildMergedContextConfiguration()} and
* {@link #getCacheAwareContextLoaderDelegate()}.
*
Concrete subclasses may choose to override this method to return a
* custom {@link TestContext} implementation.
* @since 4.2
*/
@Override
public TestContext buildTestContext() {
return new DefaultTestContext(getBootstrapContext().getTestClass(), buildMergedContextConfiguration(),
getCacheAwareContextLoaderDelegate());
}
@Override
public final List getTestExecutionListeners() {
Class clazz = getBootstrapContext().getTestClass();
Class annotationType = TestExecutionListeners.class;
List> classesList = new ArrayList<>();
boolean usingDefaults = false;
AnnotationDescriptor descriptor =
MetaAnnotationUtils.findAnnotationDescriptor(clazz, annotationType);
// Use defaults?
if (descriptor == null) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("@TestExecutionListeners is not present for class [%s]: using defaults.",
clazz.getName()));
}
usingDefaults = true;
classesList.addAll(getDefaultTestExecutionListenerClasses());
}
else {
// Traverse the class hierarchy...
while (descriptor != null) {
Class declaringClass = descriptor.getDeclaringClass();
TestExecutionListeners testExecutionListeners = descriptor.synthesizeAnnotation();
if (logger.isTraceEnabled()) {
logger.trace(String.format("Retrieved @TestExecutionListeners [%s] for declaring class [%s].",
testExecutionListeners, declaringClass.getName()));
}
boolean inheritListeners = testExecutionListeners.inheritListeners();
AnnotationDescriptor superDescriptor =
MetaAnnotationUtils.findAnnotationDescriptor(
descriptor.getRootDeclaringClass().getSuperclass(), annotationType);
// If there are no listeners to inherit, we might need to merge the
// locally declared listeners with the defaults.
if ((!inheritListeners || superDescriptor == null) &&
testExecutionListeners.mergeMode() == MergeMode.MERGE_WITH_DEFAULTS) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("Merging default listeners with listeners configured via " +
"@TestExecutionListeners for class [%s].", descriptor.getRootDeclaringClass().getName()));
}
usingDefaults = true;
classesList.addAll(getDefaultTestExecutionListenerClasses());
}
classesList.addAll(0, Arrays.asList(testExecutionListeners.listeners()));
descriptor = (inheritListeners ? superDescriptor : null);
}
}
Collection> classesToUse = classesList;
// Remove possible duplicates if we loaded default listeners.
if (usingDefaults) {
classesToUse = new LinkedHashSet<>(classesList);
}
List listeners = instantiateListeners(classesToUse);
// Sort by Ordered/@Order if we loaded default listeners.
if (usingDefaults) {
AnnotationAwareOrderComparator.sort(listeners);
}
if (logger.isInfoEnabled()) {
logger.info("Using TestExecutionListeners: " + listeners);
}
return listeners;
}
private List instantiateListeners(Collection> classes) {
List listeners = new ArrayList<>(classes.size());
for (Class listenerClass : classes) {
try {
listeners.add(BeanUtils.instantiateClass(listenerClass));
}
catch (BeanInstantiationException ex) {
if (ex.getCause() instanceof NoClassDefFoundError) {
// TestExecutionListener not applicable due to a missing dependency
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"Skipping candidate TestExecutionListener [%s] due to a missing dependency. " +
"Specify custom listener classes or make the default listener classes " +
"and their required dependencies available. Offending class: [%s]",
listenerClass.getName(), ex.getCause().getMessage()));
}
}
else {
throw ex;
}
}
}
return listeners;
}
/**
* Get the default {@link TestExecutionListener} classes for this bootstrapper.
* This method is invoked by {@link #getTestExecutionListeners()} and
* delegates to {@link #getDefaultTestExecutionListenerClassNames()} to
* retrieve the class names.
*
If a particular class cannot be loaded, a {@code DEBUG} message will
* be logged, but the associated exception will not be rethrown.
*/
@SuppressWarnings("unchecked")
protected Set> getDefaultTestExecutionListenerClasses() {
Set> defaultListenerClasses = new LinkedHashSet<>();
ClassLoader cl = getClass().getClassLoader();
for (String className : getDefaultTestExecutionListenerClassNames()) {
try {
defaultListenerClasses.add((Class) ClassUtils.forName(className, cl));
}
catch (Throwable ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not load default TestExecutionListener class [" + className +
"]. Specify custom listener classes or make the default listener classes available.", ex);
}
}
}
return defaultListenerClasses;
}
/**
* Get the names of the default {@link TestExecutionListener} classes for
* this bootstrapper.
* The default implementation looks up all
* {@code org.springframework.test.context.TestExecutionListener} entries
* configured in all {@code META-INF/spring.factories} files on the classpath.
*
This method is invoked by {@link #getDefaultTestExecutionListenerClasses()}.
* @return an unmodifiable list of names of default {@code TestExecutionListener}
* classes
* @see SpringFactoriesLoader#loadFactoryNames
*/
protected List getDefaultTestExecutionListenerClassNames() {
List classNames =
SpringFactoriesLoader.loadFactoryNames(TestExecutionListener.class, getClass().getClassLoader());
if (logger.isInfoEnabled()) {
logger.info(String.format("Loaded default TestExecutionListener class names from location [%s]: %s",
SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION, classNames));
}
return Collections.unmodifiableList(classNames);
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public final MergedContextConfiguration buildMergedContextConfiguration() {
Class testClass = getBootstrapContext().getTestClass();
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = getCacheAwareContextLoaderDelegate();
if (MetaAnnotationUtils.findAnnotationDescriptorForTypes(
testClass, ContextConfiguration.class, ContextHierarchy.class) == null) {
return buildDefaultMergedContextConfiguration(testClass, cacheAwareContextLoaderDelegate);
}
if (AnnotationUtils.findAnnotation(testClass, ContextHierarchy.class) != null) {
Map> hierarchyMap =
ContextLoaderUtils.buildContextHierarchyMap(testClass);
MergedContextConfiguration parentConfig = null;
MergedContextConfiguration mergedConfig = null;
for (List list : hierarchyMap.values()) {
List reversedList = new ArrayList<>(list);
Collections.reverse(reversedList);
// Don't use the supplied testClass; instead ensure that we are
// building the MCC for the actual test class that declared the
// configuration for the current level in the context hierarchy.
Assert.notEmpty(reversedList, "ContextConfigurationAttributes list must not be empty");
Class declaringClass = reversedList.get(0).getDeclaringClass();
mergedConfig = buildMergedContextConfiguration(
declaringClass, reversedList, parentConfig, cacheAwareContextLoaderDelegate, true);
parentConfig = mergedConfig;
}
// Return the last level in the context hierarchy
Assert.state(mergedConfig != null, "No merged context configuration");
return mergedConfig;
}
else {
return buildMergedContextConfiguration(testClass,
ContextLoaderUtils.resolveContextConfigurationAttributes(testClass),
null, cacheAwareContextLoaderDelegate, true);
}
}
private MergedContextConfiguration buildDefaultMergedContextConfiguration(Class testClass,
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {
List 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,
* and parent context configuration.
* @param testClass the test class for which the {@code MergedContextConfiguration}
* should be built (must not be {@code null})
* @param configAttributesList the list of context configuration attributes for the
* specified test class, ordered bottom-up (i.e., as if we were
* traversing up the class hierarchy); never {@code null} or empty
* @param parentConfig the merged context configuration for the parent application
* 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
* @see SmartContextLoader#processContextConfiguration
* @see ContextLoader#processLocations
* @see ActiveProfilesUtils#resolveActiveProfiles
* @see ApplicationContextInitializerUtils#resolveInitializerClasses
* @see MergedContextConfiguration
*/
private MergedContextConfiguration buildMergedContextConfiguration(Class testClass,
List configAttributesList, @Nullable MergedContextConfiguration parentConfig,
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate,
boolean requireLocationsClassesOrInitializers) {
Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be null or empty");
ContextLoader contextLoader = resolveContextLoader(testClass, configAttributesList);
List locations = new ArrayList<>();
List> classes = new ArrayList<>();
List> initializers = new ArrayList<>();
for (ContextConfigurationAttributes configAttributes : configAttributesList) {
if (logger.isTraceEnabled()) {
logger.trace(String.format("Processing locations and classes for context configuration attributes %s",
configAttributes));
}
if (contextLoader instanceof SmartContextLoader) {
SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader;
smartContextLoader.processContextConfiguration(configAttributes);
locations.addAll(0, Arrays.asList(configAttributes.getLocations()));
classes.addAll(0, Arrays.asList(configAttributes.getClasses()));
}
else {
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;
}
}
Set contextCustomizers = getContextCustomizers(testClass,
Collections.unmodifiableList(configAttributesList));
Assert.state(!(requireLocationsClassesOrInitializers &&
areAllEmpty(locations, classes, initializers, contextCustomizers)), () -> String.format(
"%s was unable to detect defaults, and no ApplicationContextInitializers " +
"or ContextCustomizers were declared for context configuration attributes %s",
contextLoader.getClass().getSimpleName(), configAttributesList));
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(),
contextCustomizers, contextLoader, cacheAwareContextLoaderDelegate, parentConfig);
return processMergedContextConfiguration(mergedConfig);
}
private Set getContextCustomizers(Class testClass,
List configAttributes) {
List factories = getContextCustomizerFactories();
Set customizers = new LinkedHashSet<>(factories.size());
for (ContextCustomizerFactory factory : factories) {
ContextCustomizer customizer = factory.createContextCustomizer(testClass, configAttributes);
if (customizer != null) {
customizers.add(customizer);
}
}
return customizers;
}
/**
* Get the {@link ContextCustomizerFactory} instances for this bootstrapper.
* The default implementation uses the {@link SpringFactoriesLoader} mechanism
* for loading factories configured in all {@code META-INF/spring.factories}
* files on the classpath.
* @since 4.3
* @see SpringFactoriesLoader#loadFactories
*/
protected List getContextCustomizerFactories() {
return SpringFactoriesLoader.loadFactories(ContextCustomizerFactory.class, getClass().getClassLoader());
}
/**
* Resolve the {@link ContextLoader} {@linkplain Class class} to use for the
* supplied list of {@link ContextConfigurationAttributes} and then instantiate
* and return that {@code ContextLoader}.
* If the user has not explicitly declared which loader to use, the value
* returned from {@link #getDefaultContextLoaderClass} will be used as the
* default context loader class. For details on the class resolution process,
* see {@link #resolveExplicitContextLoaderClass} and
* {@link #getDefaultContextLoaderClass}.
* @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}; must be ordered bottom-up
* (i.e., as if we were traversing up the class hierarchy)
* @return the resolved {@code ContextLoader} for the supplied {@code testClass}
* (never {@code null})
* @throws IllegalStateException if {@link #getDefaultContextLoaderClass(Class)}
* returns {@code null}
*/
protected ContextLoader resolveContextLoader(Class testClass,
List configAttributesList) {
Assert.notNull(testClass, "Class must not be null");
Assert.notNull(configAttributesList, "ContextConfigurationAttributes list must not be null");
Class contextLoaderClass = resolveExplicitContextLoaderClass(configAttributesList);
if (contextLoaderClass == null) {
contextLoaderClass = getDefaultContextLoaderClass(testClass);
}
if (logger.isTraceEnabled()) {
logger.trace(String.format("Using ContextLoader class [%s] for test class [%s]",
contextLoaderClass.getName(), testClass.getName()));
}
return BeanUtils.instantiateClass(contextLoaderClass, ContextLoader.class);
}
/**
* Resolve the {@link ContextLoader} {@linkplain Class class} to use for the supplied
* list of {@link ContextConfigurationAttributes}.
* Beginning with the first level in the context configuration attributes hierarchy:
*
* - If the {@link ContextConfigurationAttributes#getContextLoaderClass()
* contextLoaderClass} property of {@link ContextConfigurationAttributes} is
* configured with an explicit class, that class will be returned.
* - If an explicit {@code ContextLoader} class is not specified at the current
* level in the hierarchy, traverse to the next level in the hierarchy and return to
* step #1.
*
* @param configAttributesList the list of configuration attributes to process;
* must not be {@code null}; must be ordered bottom-up
* (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
* @throws IllegalArgumentException if supplied configuration attributes are
* {@code null} or empty
*/
@Nullable
protected Class resolveExplicitContextLoaderClass(
List configAttributesList) {
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",
configAttributes));
}
Class contextLoaderClass = configAttributes.getContextLoaderClass();
if (ContextLoader.class != contextLoaderClass) {
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"Found explicit ContextLoader class [%s] for context configuration attributes %s",
contextLoaderClass.getName(), configAttributes));
}
return contextLoaderClass;
}
}
return null;
}
/**
* Get the {@link CacheAwareContextLoaderDelegate} to use for transparent
* interaction with the {@code ContextCache}.
* The default implementation simply delegates to
* {@code getBootstrapContext().getCacheAwareContextLoaderDelegate()}.
*
Concrete subclasses may choose to override this method to return a custom
* {@code CacheAwareContextLoaderDelegate} implementation with custom
* {@link org.springframework.test.context.cache.ContextCache ContextCache} support.
* @return the context loader delegate (never {@code null})
*/
protected CacheAwareContextLoaderDelegate getCacheAwareContextLoaderDelegate() {
return getBootstrapContext().getCacheAwareContextLoaderDelegate();
}
/**
* Determine the default {@link ContextLoader} {@linkplain Class class}
* to use for the supplied test class.
*
The class returned by this method will only be used if a {@code ContextLoader}
* class has not been explicitly declared via {@link ContextConfiguration#loader}.
* @param testClass the test class for which to retrieve the default
* {@code ContextLoader} class
* @return the default {@code ContextLoader} class for the supplied test class
* (never {@code null})
*/
protected abstract Class getDefaultContextLoaderClass(Class testClass);
/**
* Process the supplied, newly instantiated {@link MergedContextConfiguration} instance.
*
The returned {@link MergedContextConfiguration} instance may be a wrapper
* around or a replacement for the original.
*
The default implementation simply returns the supplied instance unmodified.
*
Concrete subclasses may choose to return a specialized subclass of
* {@link MergedContextConfiguration} based on properties in the supplied instance.
* @param mergedConfig the {@code MergedContextConfiguration} to process; never {@code null}
* @return a fully initialized {@code MergedContextConfiguration}; never {@code null}
*/
protected MergedContextConfiguration processMergedContextConfiguration(MergedContextConfiguration mergedConfig) {
return mergedConfig;
}
private static boolean areAllEmpty(Collection... collections) {
return Arrays.stream(collections).allMatch(Collection::isEmpty);
}
}