/*
* 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.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.SpringApplication;
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.mock.web.SpringBootMockServletContext;
import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.boot.web.reactive.context.GenericReactiveWebApplicationContext;
import org.springframework.boot.web.servlet.support.ServletContextApplicationContextInitializer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.SpringVersion;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextLoader;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.support.AbstractContextLoader;
import org.springframework.test.context.support.AnnotationConfigContextLoaderUtils;
import org.springframework.test.context.support.TestPropertySourceUtils;
import org.springframework.test.context.web.WebMergedContextConfiguration;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.support.GenericWebApplicationContext;
/**
* A {@link ContextLoader} that can be used to test Spring Boot applications (those that
* normally startup using {@link SpringApplication}). Although this loader can be used
* directly, most test will instead want to use it with {@link SpringBootTest}.
* <p>
* The loader supports both standard {@link MergedContextConfiguration} as well as
* {@link WebMergedContextConfiguration}. If {@link WebMergedContextConfiguration} is used
* the context will either use a mock servlet environment, or start the full embedded web
* server.
* <p>
* If {@code @ActiveProfiles} are provided in the test class they will be used to create
* the application context.
*
* @author Dave Syer
* @author Phillip Webb
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Madhura Bhave
* @see SpringBootTest
*/
public class SpringBootContextLoader extends AbstractContextLoader {
private static final Set<String> INTEGRATION_TEST_ANNOTATIONS;
static {
Set<String> annotations = new LinkedHashSet<>();
annotations.add("org.springframework.boot.test.IntegrationTest");
annotations.add("org.springframework.boot.test.WebIntegrationTest");
INTEGRATION_TEST_ANNOTATIONS = Collections.unmodifiableSet(annotations);
}
@Override
public ApplicationContext loadContext(MergedContextConfiguration config)
throws Exception {
SpringApplication application = getSpringApplication();
application.setMainApplicationClass(config.getTestClass());
application.setSources(getSources(config));
ConfigurableEnvironment environment = new StandardEnvironment();
if (!ObjectUtils.isEmpty(config.getActiveProfiles())) {
setActiveProfiles(environment, config.getActiveProfiles());
}
TestPropertySourceUtils.addPropertiesFilesToEnvironment(environment,
application.getResourceLoader() == null
? new DefaultResourceLoader(getClass().getClassLoader())
: application.getResourceLoader(),
config.getPropertySourceLocations());
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment,
getInlinedProperties(config));
application.setEnvironment(environment);
List<ApplicationContextInitializer<?>> initializers = getInitializers(config,
application);
if (config instanceof WebMergedContextConfiguration) {
application.setWebApplicationType(WebApplicationType.SERVLET);
if (!isEmbeddedWebEnvironment(config)) {
new WebConfigurer().configure(config, application, initializers);
}
}
else if (config instanceof ReactiveWebMergedContextConfiguration) {
application.setWebApplicationType(WebApplicationType.REACTIVE);
if (!isEmbeddedWebEnvironment(config)) {
new ReactiveWebConfigurer().configure(application);
}
}
else {
application.setWebApplicationType(WebApplicationType.NONE);
}
application.setInitializers(initializers);
ConfigurableApplicationContext context = application.run();
return context;
}
/**
* Builds new {@link org.springframework.boot.SpringApplication} instance. You can
* override this method to add custom behavior
* @return {@link org.springframework.boot.SpringApplication} instance
*/
protected SpringApplication getSpringApplication() {
return new SpringApplication();
}
private Set<Object> getSources(MergedContextConfiguration mergedConfig) {
Set<Object> sources = new LinkedHashSet<>();
sources.addAll(Arrays.asList(mergedConfig.getClasses()));
sources.addAll(Arrays.asList(mergedConfig.getLocations()));
Assert.state(!sources.isEmpty(), "No configuration classes "
+ "or locations found in @SpringApplicationConfiguration. "
+ "For default configuration detection to work you need "
+ "Spring 4.0.3 or better (found " + SpringVersion.getVersion() + ").");
return sources;
}
private void setActiveProfiles(ConfigurableEnvironment environment,
String[] profiles) {
EnvironmentTestUtils.addEnvironment(environment, "spring.profiles.active="
+ StringUtils.arrayToCommaDelimitedString(profiles));
}
protected String[] getInlinedProperties(MergedContextConfiguration config) {
ArrayList<String> properties = new ArrayList<>();
// JMX bean names will clash if the same bean is used in multiple contexts
disableJmx(properties);
properties.addAll(Arrays.asList(config.getPropertySourceProperties()));
if (!isEmbeddedWebEnvironment(config) && !hasCustomServerPort(properties)) {
properties.add("server.port=-1");
}
return properties.toArray(new String[properties.size()]);
}
private void disableJmx(List<String> properties) {
properties.add("spring.jmx.enabled=false");
}
private boolean hasCustomServerPort(List<String> properties) {
Binder binder = new Binder(convertToConfigurationPropertySource(properties));
return binder.bind("server.port", Bindable.of(String.class)).isBound();
}
private ConfigurationPropertySource convertToConfigurationPropertySource(
List<String> properties) {
String[] array = properties.toArray(new String[properties.size()]);
return new MapConfigurationPropertySource(
TestPropertySourceUtils.convertInlinedPropertiesToMap(array));
}
private List<ApplicationContextInitializer<?>> getInitializers(
MergedContextConfiguration config, SpringApplication application) {
List<ApplicationContextInitializer<?>> initializers = new ArrayList<>();
for (ContextCustomizer contextCustomizer : config.getContextCustomizers()) {
initializers.add(new ContextCustomizerAdapter(contextCustomizer, config));
}
initializers.addAll(application.getInitializers());
for (Class<? extends ApplicationContextInitializer<?>> initializerClass : config
.getContextInitializerClasses()) {
initializers.add(BeanUtils.instantiateClass(initializerClass));
}
if (config.getParent() != null) {
initializers.add(new ParentContextApplicationContextInitializer(
config.getParentApplicationContext()));
}
return initializers;
}
private boolean isEmbeddedWebEnvironment(MergedContextConfiguration config) {
for (String annotation : INTEGRATION_TEST_ANNOTATIONS) {
if (AnnotatedElementUtils.isAnnotated(config.getTestClass(), annotation)) {
return true;
}
}
SpringBootTest annotation = AnnotatedElementUtils
.findMergedAnnotation(config.getTestClass(), SpringBootTest.class);
if (annotation != null && annotation.webEnvironment().isEmbedded()) {
return true;
}
return false;
}
@Override
public void processContextConfiguration(
ContextConfigurationAttributes configAttributes) {
super.processContextConfiguration(configAttributes);
if (!configAttributes.hasResources()) {
Class<?>[] defaultConfigClasses = detectDefaultConfigurationClasses(
configAttributes.getDeclaringClass());
configAttributes.setClasses(defaultConfigClasses);
}
}
/**
* Detect the default configuration classes for the supplied test class. By default
* simply delegates to
* {@link AnnotationConfigContextLoaderUtils#detectDefaultConfigurationClasses} .
* @param declaringClass the test class that declared {@code @ContextConfiguration}
* @return an array of default configuration classes, potentially empty but never
* {@code null}
* @see AnnotationConfigContextLoaderUtils
*/
protected Class<?>[] detectDefaultConfigurationClasses(Class<?> declaringClass) {
return AnnotationConfigContextLoaderUtils
.detectDefaultConfigurationClasses(declaringClass);
}
@Override
public ApplicationContext loadContext(String... locations) throws Exception {
throw new UnsupportedOperationException("SpringApplicationContextLoader "
+ "does not support the loadContext(String...) method");
}
@Override
protected String[] getResourceSuffixes() {
return new String[] { "-context.xml", "Context.groovy" };
}
@Override
protected String getResourceSuffix() {
throw new IllegalStateException();
}
/**
* Inner class to configure {@link WebMergedContextConfiguration}.
*/
private static class WebConfigurer {
private static final Class<GenericWebApplicationContext> WEB_CONTEXT_CLASS = GenericWebApplicationContext.class;
void configure(MergedContextConfiguration configuration,
SpringApplication application,
List<ApplicationContextInitializer<?>> initializers) {
WebMergedContextConfiguration webConfiguration = (WebMergedContextConfiguration) configuration;
addMockServletContext(initializers, webConfiguration);
application.setApplicationContextClass(WEB_CONTEXT_CLASS);
}
private void addMockServletContext(
List<ApplicationContextInitializer<?>> initializers,
WebMergedContextConfiguration webConfiguration) {
SpringBootMockServletContext servletContext = new SpringBootMockServletContext(
webConfiguration.getResourceBasePath());
initializers.add(0, new ServletContextApplicationContextInitializer(
servletContext, true));
}
}
/**
* Inner class to configure {@link ReactiveWebMergedContextConfiguration}.
*/
private static class ReactiveWebConfigurer {
private static final Class<GenericReactiveWebApplicationContext> WEB_CONTEXT_CLASS = GenericReactiveWebApplicationContext.class;
void configure(SpringApplication application) {
application.setApplicationContextClass(WEB_CONTEXT_CLASS);
}
}
/**
* Adapts a {@link ContextCustomizer} to a {@link ApplicationContextInitializer} so
* that it can be triggered via {@link SpringApplication}.
*/
private static class ContextCustomizerAdapter
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
private final ContextCustomizer contextCustomizer;
private final MergedContextConfiguration config;
ContextCustomizerAdapter(ContextCustomizer contextCustomizer,
MergedContextConfiguration config) {
this.contextCustomizer = contextCustomizer;
this.config = config;
}
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
this.contextCustomizer.customizeContext(applicationContext, this.config);
}
}
@Order(Ordered.HIGHEST_PRECEDENCE)
private static class ParentContextApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
private final ApplicationContext parent;
ParentContextApplicationContextInitializer(ApplicationContext parent) {
this.parent = parent;
}
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.setParent(this.parent);
}
}
}