All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.test.context.support.AbstractTestContextBootstrapper Maven / Gradle / Ivy

There is a newer version: 6.1.6
Show newest version
/*
 * 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: *

    *
  1. If the {@link ContextConfigurationAttributes#getContextLoaderClass() * contextLoaderClass} property of {@link ContextConfigurationAttributes} is * configured with an explicit class, that class will be returned.
  2. *
  3. 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.
  4. *
* @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); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy