/*
* Copyright 2012-2017 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
*
* http://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.boot.test.context;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextHierarchy;
import org.springframework.test.context.ContextLoader;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestContextBootstrapper;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.support.DefaultTestContextBootstrapper;
import org.springframework.test.context.support.TestPropertySourceUtils;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.context.web.WebMergedContextConfiguration;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* {@link TestContextBootstrapper} for Spring Boot. Provides support for
* {@link SpringBootTest @SpringBootTest} and may also be used directly or subclassed.
* Provides the following features over and above {@link DefaultTestContextBootstrapper}:
* <ul>
* <li>Uses {@link SpringBootContextLoader} as the
* {@link #getDefaultContextLoaderClass(Class) default context loader}.</li>
* <li>Automatically searches for a
* {@link SpringBootConfiguration @SpringBootConfiguration} when required.</li>
* <li>Allows custom {@link Environment} {@link #getProperties(Class)} to be defined.</li>
* <li>Provides support for different {@link WebEnvironment webEnvironment} modes.</li>
* </ul>
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Brian Clozel
* @author Madhura Bhave
* @since 1.4.0
* @see SpringBootTest
* @see TestConfiguration
*/
public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstrapper {
private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet",
"org.springframework.web.context.ConfigurableWebApplicationContext" };
private static final String REACTIVE_WEB_ENVIRONMENT_CLASS = "org.springframework."
+ "web.reactive.DispatcherHandler";
private static final String MVC_WEB_ENVIRONMENT_CLASS = "org.springframework."
+ "web.servlet.DispatcherServlet";
private static final String ACTIVATE_SERVLET_LISTENER = "org.springframework.test."
+ "context.web.ServletTestExecutionListener.activateListener";
private static final Log logger = LogFactory
.getLog(SpringBootTestContextBootstrapper.class);
@Override
public TestContext buildTestContext() {
TestContext context = super.buildTestContext();
verifyConfiguration(context.getTestClass());
WebEnvironment webEnvironment = getWebEnvironment(context.getTestClass());
if (webEnvironment == WebEnvironment.MOCK
&& deduceWebApplicationType() == WebApplicationType.SERVLET) {
context.setAttribute(ACTIVATE_SERVLET_LISTENER, true);
}
else if (webEnvironment != null && webEnvironment.isEmbedded()) {
context.setAttribute(ACTIVATE_SERVLET_LISTENER, false);
}
return context;
}
@Override
protected Set<Class<? extends TestExecutionListener>> getDefaultTestExecutionListenerClasses() {
Set<Class<? extends TestExecutionListener>> listeners = super.getDefaultTestExecutionListenerClasses();
List<DefaultTestExecutionListenersPostProcessor> postProcessors = SpringFactoriesLoader
.loadFactories(DefaultTestExecutionListenersPostProcessor.class,
getClass().getClassLoader());
for (DefaultTestExecutionListenersPostProcessor postProcessor : postProcessors) {
listeners = postProcessor.postProcessDefaultTestExecutionListeners(listeners);
}
return listeners;
}
@Override
protected ContextLoader resolveContextLoader(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributesList) {
Class<?>[] classes = getClasses(testClass);
if (!ObjectUtils.isEmpty(classes)) {
for (ContextConfigurationAttributes configAttributes : configAttributesList) {
addConfigAttributesClasses(configAttributes, classes);
}
}
return super.resolveContextLoader(testClass, configAttributesList);
}
private void addConfigAttributesClasses(
ContextConfigurationAttributes configAttributes, Class<?>[] classes) {
List<Class<?>> combined = new ArrayList<>();
combined.addAll(Arrays.asList(classes));
if (configAttributes.getClasses() != null) {
combined.addAll(Arrays.asList(configAttributes.getClasses()));
}
configAttributes.setClasses(combined.toArray(new Class<?>[combined.size()]));
}
@Override
protected Class<? extends ContextLoader> getDefaultContextLoaderClass(
Class<?> testClass) {
return SpringBootContextLoader.class;
}
@Override
protected MergedContextConfiguration processMergedContextConfiguration(
MergedContextConfiguration mergedConfig) {
Class<?>[] classes = getOrFindConfigurationClasses(mergedConfig);
List<String> propertySourceProperties = getAndProcessPropertySourceProperties(
mergedConfig);
mergedConfig = createModifiedConfig(mergedConfig, classes,
propertySourceProperties
.toArray(new String[propertySourceProperties.size()]));
WebEnvironment webEnvironment = getWebEnvironment(mergedConfig.getTestClass());
if (webEnvironment != null && isWebEnvironmentSupported(mergedConfig)) {
WebApplicationType webApplicationType = getWebApplicationType(mergedConfig);
if (webApplicationType == WebApplicationType.SERVLET
&& (webEnvironment.isEmbedded()
|| webEnvironment == WebEnvironment.MOCK)) {
WebAppConfiguration webAppConfiguration = AnnotatedElementUtils
.findMergedAnnotation(mergedConfig.getTestClass(),
WebAppConfiguration.class);
String resourceBasePath = (webAppConfiguration == null ? "src/main/webapp"
: webAppConfiguration.value());
mergedConfig = new WebMergedContextConfiguration(mergedConfig,
resourceBasePath);
}
else if (webApplicationType == WebApplicationType.REACTIVE
&& (webEnvironment.isEmbedded()
|| webEnvironment == WebEnvironment.MOCK)) {
return new ReactiveWebMergedContextConfiguration(mergedConfig);
}
}
return mergedConfig;
}
private WebApplicationType getWebApplicationType(
MergedContextConfiguration configuration) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(
TestPropertySourceUtils.convertInlinedPropertiesToMap(
configuration.getPropertySourceProperties()));
Binder binder = new Binder(source);
return binder
.bind("spring.main.web-application-type",
Bindable.of(WebApplicationType.class))
.orElseGet(this::deduceWebApplicationType);
}
private WebApplicationType deduceWebApplicationType() {
if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null)
&& !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : WEB_ENVIRONMENT_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
private boolean isWebEnvironmentSupported(MergedContextConfiguration mergedConfig) {
Class<?> testClass = mergedConfig.getTestClass();
ContextHierarchy hierarchy = AnnotationUtils.getAnnotation(testClass,
ContextHierarchy.class);
if (hierarchy == null || hierarchy.value().length == 0) {
return true;
}
ContextConfiguration[] configurations = hierarchy.value();
return isFromConfiguration(mergedConfig,
configurations[configurations.length - 1]);
}
private boolean isFromConfiguration(MergedContextConfiguration candidateConfig,
ContextConfiguration configuration) {
ContextConfigurationAttributes attributes = new ContextConfigurationAttributes(
candidateConfig.getTestClass(), configuration);
Set<Class<?>> configurationClasses = new HashSet<Class<?>>(
Arrays.asList(attributes.getClasses()));
for (Class<?> candidate : candidateConfig.getClasses()) {
if (configurationClasses.contains(candidate)) {
return true;
}
}
return false;
}
protected Class<?>[] getOrFindConfigurationClasses(
MergedContextConfiguration mergedConfig) {
Class<?>[] classes = mergedConfig.getClasses();
if (containsNonTestComponent(classes) || mergedConfig.hasLocations()) {
return classes;
}
Class<?> found = new SpringBootConfigurationFinder()
.findFromClass(mergedConfig.getTestClass());
Assert.state(found != null,
"Unable to find a @SpringBootConfiguration, you need to use "
+ "@ContextConfiguration or @SpringBootTest(classes=...) "
+ "with your test");
logger.info("Found @SpringBootConfiguration " + found.getName() + " for test "
+ mergedConfig.getTestClass());
return merge(found, classes);
}
private boolean containsNonTestComponent(Class<?>[] classes) {
for (Class<?> candidate : classes) {
if (!AnnotatedElementUtils.isAnnotated(candidate, TestConfiguration.class)) {
return true;
}
}
return false;
}
private Class<?>[] merge(Class<?> head, Class<?>[] existing) {
Class<?>[] result = new Class<?>[existing.length + 1];
result[0] = head;
System.arraycopy(existing, 0, result, 1, existing.length);
return result;
}
private List<String> getAndProcessPropertySourceProperties(
MergedContextConfiguration mergedConfig) {
List<String> propertySourceProperties = new ArrayList<>(
Arrays.asList(mergedConfig.getPropertySourceProperties()));
String differentiator = getDifferentiatorPropertySourceProperty();
if (differentiator != null) {
propertySourceProperties.add(differentiator);
}
processPropertySourceProperties(mergedConfig, propertySourceProperties);
return propertySourceProperties;
}
/**
* Return a "differentiator" property to ensure that there is something to
* differentiate regular tests and bootstrapped tests. Without this property a cached
* context could be returned that wasn't created by this bootstrapper. By default uses
* the bootstrapper class as a property.
* @return the differentiator or {@code null}
*/
protected String getDifferentiatorPropertySourceProperty() {
return getClass().getName() + "=true";
}
/**
* Post process the property source properties, adding or removing elements as
* required.
* @param mergedConfig the merged context configuration
* @param propertySourceProperties the property source properties to process
*/
protected void processPropertySourceProperties(
MergedContextConfiguration mergedConfig,
List<String> propertySourceProperties) {
Class<?> testClass = mergedConfig.getTestClass();
String[] properties = getProperties(testClass);
if (!ObjectUtils.isEmpty(properties)) {
// Added first so that inlined properties from @TestPropertySource take
// precedence
propertySourceProperties.addAll(0, Arrays.asList(properties));
}
if (getWebEnvironment(testClass) == WebEnvironment.RANDOM_PORT) {
propertySourceProperties.add("server.port=0");
}
}
/**
* Return the {@link WebEnvironment} type for this test or null if undefined.
* @param testClass the source test class
* @return the {@link WebEnvironment} or {@code null}
*/
protected WebEnvironment getWebEnvironment(Class<?> testClass) {
SpringBootTest annotation = getAnnotation(testClass);
return (annotation == null ? null : annotation.webEnvironment());
}
protected Class<?>[] getClasses(Class<?> testClass) {
SpringBootTest annotation = getAnnotation(testClass);
return (annotation == null ? null : annotation.classes());
}
protected String[] getProperties(Class<?> testClass) {
SpringBootTest annotation = getAnnotation(testClass);
return (annotation == null ? null : annotation.properties());
}
protected SpringBootTest getAnnotation(Class<?> testClass) {
return AnnotatedElementUtils.getMergedAnnotation(testClass, SpringBootTest.class);
}
protected void verifyConfiguration(Class<?> testClass) {
SpringBootTest springBootTest = getAnnotation(testClass);
if (springBootTest != null
&& (springBootTest.webEnvironment() == WebEnvironment.DEFINED_PORT
|| springBootTest.webEnvironment() == WebEnvironment.RANDOM_PORT)
&& getAnnotation(WebAppConfiguration.class, testClass) != null) {
throw new IllegalStateException("@WebAppConfiguration should only be used "
+ "with @SpringBootTest when @SpringBootTest is configured with a "
+ "mock web environment. Please remove @WebAppConfiguration or "
+ "reconfigure @SpringBootTest.");
}
}
private <T extends Annotation> T getAnnotation(Class<T> annotationType,
Class<?> testClass) {
return AnnotatedElementUtils.getMergedAnnotation(testClass, annotationType);
}
/**
* Create a new {@link MergedContextConfiguration} with different classes.
* @param mergedConfig the source config
* @param classes the replacement classes
* @return a new {@link MergedContextConfiguration}
*/
protected final MergedContextConfiguration createModifiedConfig(
MergedContextConfiguration mergedConfig, Class<?>[] classes) {
return createModifiedConfig(mergedConfig, classes,
mergedConfig.getPropertySourceProperties());
}
/**
* Create a new {@link MergedContextConfiguration} with different classes and
* properties.
* @param mergedConfig the source config
* @param classes the replacement classes
* @param propertySourceProperties the replacement properties
* @return a new {@link MergedContextConfiguration}
*/
protected final MergedContextConfiguration createModifiedConfig(
MergedContextConfiguration mergedConfig, Class<?>[] classes,
String[] propertySourceProperties) {
return new MergedContextConfiguration(mergedConfig.getTestClass(),
mergedConfig.getLocations(), classes,
mergedConfig.getContextInitializerClasses(),
mergedConfig.getActiveProfiles(),
mergedConfig.getPropertySourceLocations(), propertySourceProperties,
mergedConfig.getContextCustomizers(), mergedConfig.getContextLoader(),
getCacheAwareContextLoaderDelegate(), mergedConfig.getParent());
}
}