001/*
002 * Copyright 2012-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.test.context;
018
019import java.lang.annotation.Annotation;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Set;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028
029import org.springframework.boot.SpringBootConfiguration;
030import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
031import org.springframework.core.annotation.AnnotatedElementUtils;
032import org.springframework.core.annotation.AnnotationUtils;
033import org.springframework.core.env.Environment;
034import org.springframework.core.io.support.SpringFactoriesLoader;
035import org.springframework.test.context.ContextConfiguration;
036import org.springframework.test.context.ContextConfigurationAttributes;
037import org.springframework.test.context.ContextHierarchy;
038import org.springframework.test.context.ContextLoader;
039import org.springframework.test.context.MergedContextConfiguration;
040import org.springframework.test.context.TestContext;
041import org.springframework.test.context.TestContextBootstrapper;
042import org.springframework.test.context.TestExecutionListener;
043import org.springframework.test.context.support.DefaultTestContextBootstrapper;
044import org.springframework.test.context.web.WebAppConfiguration;
045import org.springframework.test.context.web.WebMergedContextConfiguration;
046import org.springframework.util.Assert;
047import org.springframework.util.ClassUtils;
048import org.springframework.util.ObjectUtils;
049
050/**
051 * {@link TestContextBootstrapper} for Spring Boot. Provides support for
052 * {@link SpringBootTest @SpringBootTest} and may also be used directly or subclassed.
053 * Provides the following features over and above {@link DefaultTestContextBootstrapper}:
054 * <ul>
055 * <li>Uses {@link SpringBootContextLoader} as the
056 * {@link #getDefaultContextLoaderClass(Class) default context loader}.</li>
057 * <li>Automatically searches for a
058 * {@link SpringBootConfiguration @SpringBootConfiguration} when required.</li>
059 * <li>Allows custom {@link Environment} {@link #getProperties(Class)} to be defined.</li>
060 * <li>Provides support for different {@link WebEnvironment webEnvironment} modes.</li>
061 * </ul>
062 *
063 * @author Phillip Webb
064 * @author Andy Wilkinson
065 * @since 1.4.0
066 * @see SpringBootTest
067 * @see TestConfiguration
068 */
069public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstrapper {
070
071        private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet",
072                        "org.springframework.web.context.ConfigurableWebApplicationContext" };
073
074        private static final String ACTIVATE_SERVLET_LISTENER = "org.springframework.test."
075                        + "context.web.ServletTestExecutionListener.activateListener";
076
077        private static final Log logger = LogFactory
078                        .getLog(SpringBootTestContextBootstrapper.class);
079
080        @Override
081        public TestContext buildTestContext() {
082                TestContext context = super.buildTestContext();
083                verifyConfiguration(context.getTestClass());
084                WebEnvironment webEnvironment = getWebEnvironment(context.getTestClass());
085                if (webEnvironment == WebEnvironment.MOCK && hasWebEnvironmentClasses()) {
086                        context.setAttribute(ACTIVATE_SERVLET_LISTENER, true);
087                }
088                else if (webEnvironment != null && webEnvironment.isEmbedded()) {
089                        context.setAttribute(ACTIVATE_SERVLET_LISTENER, false);
090                }
091                return context;
092        }
093
094        @Override
095        protected Set<Class<? extends TestExecutionListener>> getDefaultTestExecutionListenerClasses() {
096                Set<Class<? extends TestExecutionListener>> listeners = super.getDefaultTestExecutionListenerClasses();
097                List<DefaultTestExecutionListenersPostProcessor> postProcessors = SpringFactoriesLoader
098                                .loadFactories(DefaultTestExecutionListenersPostProcessor.class,
099                                                getClass().getClassLoader());
100                for (DefaultTestExecutionListenersPostProcessor postProcessor : postProcessors) {
101                        listeners = postProcessor.postProcessDefaultTestExecutionListeners(listeners);
102                }
103                return listeners;
104        }
105
106        @Override
107        protected ContextLoader resolveContextLoader(Class<?> testClass,
108                        List<ContextConfigurationAttributes> configAttributesList) {
109                Class<?>[] classes = getClasses(testClass);
110                if (!ObjectUtils.isEmpty(classes)) {
111                        for (ContextConfigurationAttributes configAttributes : configAttributesList) {
112                                addConfigAttributesClasses(configAttributes, classes);
113                        }
114                }
115                return super.resolveContextLoader(testClass, configAttributesList);
116        }
117
118        private void addConfigAttributesClasses(
119                        ContextConfigurationAttributes configAttributes, Class<?>[] classes) {
120                List<Class<?>> combined = new ArrayList<Class<?>>();
121                combined.addAll(Arrays.asList(classes));
122                if (configAttributes.getClasses() != null) {
123                        combined.addAll(Arrays.asList(configAttributes.getClasses()));
124                }
125                configAttributes.setClasses(combined.toArray(new Class<?>[combined.size()]));
126        }
127
128        @Override
129        protected Class<? extends ContextLoader> getDefaultContextLoaderClass(
130                        Class<?> testClass) {
131                return SpringBootContextLoader.class;
132        }
133
134        @Override
135        protected MergedContextConfiguration processMergedContextConfiguration(
136                        MergedContextConfiguration mergedConfig) {
137                Class<?>[] classes = getOrFindConfigurationClasses(mergedConfig);
138                List<String> propertySourceProperties = getAndProcessPropertySourceProperties(
139                                mergedConfig);
140                mergedConfig = createModifiedConfig(mergedConfig, classes,
141                                propertySourceProperties
142                                                .toArray(new String[propertySourceProperties.size()]));
143                WebEnvironment webEnvironment = getWebEnvironment(mergedConfig.getTestClass());
144                if (webEnvironment != null && isWebEnvironmentSupported(mergedConfig)) {
145                        if (webEnvironment.isEmbedded() || (webEnvironment == WebEnvironment.MOCK
146                                        && hasWebEnvironmentClasses())) {
147                                WebAppConfiguration webAppConfiguration = AnnotatedElementUtils
148                                                .findMergedAnnotation(mergedConfig.getTestClass(),
149                                                                WebAppConfiguration.class);
150                                String resourceBasePath = (webAppConfiguration == null ? "src/main/webapp"
151                                                : webAppConfiguration.value());
152                                mergedConfig = new WebMergedContextConfiguration(mergedConfig,
153                                                resourceBasePath);
154                        }
155                }
156                return mergedConfig;
157        }
158
159        private boolean isWebEnvironmentSupported(MergedContextConfiguration mergedConfig) {
160                Class<?> testClass = mergedConfig.getTestClass();
161                ContextHierarchy hierarchy = AnnotationUtils.getAnnotation(testClass,
162                                ContextHierarchy.class);
163                if (hierarchy == null || hierarchy.value().length == 0) {
164                        return true;
165                }
166                ContextConfiguration[] configurations = hierarchy.value();
167                return isFromConfiguration(mergedConfig,
168                                configurations[configurations.length - 1]);
169        }
170
171        private boolean isFromConfiguration(MergedContextConfiguration candidateConfig,
172                        ContextConfiguration configuration) {
173                ContextConfigurationAttributes attributes = new ContextConfigurationAttributes(
174                                candidateConfig.getTestClass(), configuration);
175                Set<Class<?>> configurationClasses = new HashSet<Class<?>>(
176                                Arrays.asList(attributes.getClasses()));
177                for (Class<?> candidate : candidateConfig.getClasses()) {
178                        if (configurationClasses.contains(candidate)) {
179                                return true;
180                        }
181                }
182                return false;
183        }
184
185        private boolean hasWebEnvironmentClasses() {
186                for (String className : WEB_ENVIRONMENT_CLASSES) {
187                        if (!ClassUtils.isPresent(className, null)) {
188                                return false;
189                        }
190                }
191                return true;
192        }
193
194        protected Class<?>[] getOrFindConfigurationClasses(
195                        MergedContextConfiguration mergedConfig) {
196                Class<?>[] classes = mergedConfig.getClasses();
197                if (containsNonTestComponent(classes) || mergedConfig.hasLocations()) {
198                        return classes;
199                }
200                Class<?> found = new SpringBootConfigurationFinder()
201                                .findFromClass(mergedConfig.getTestClass());
202                Assert.state(found != null,
203                                "Unable to find a @SpringBootConfiguration, you need to use "
204                                                + "@ContextConfiguration or @SpringBootTest(classes=...) "
205                                                + "with your test");
206                logger.info("Found @SpringBootConfiguration " + found.getName() + " for test "
207                                + mergedConfig.getTestClass());
208                return merge(found, classes);
209        }
210
211        private boolean containsNonTestComponent(Class<?>[] classes) {
212                for (Class<?> candidate : classes) {
213                        if (!AnnotatedElementUtils.isAnnotated(candidate, TestConfiguration.class)) {
214                                return true;
215                        }
216                }
217                return false;
218        }
219
220        private Class<?>[] merge(Class<?> head, Class<?>[] existing) {
221                Class<?>[] result = new Class<?>[existing.length + 1];
222                result[0] = head;
223                System.arraycopy(existing, 0, result, 1, existing.length);
224                return result;
225        }
226
227        private List<String> getAndProcessPropertySourceProperties(
228                        MergedContextConfiguration mergedConfig) {
229                List<String> propertySourceProperties = new ArrayList<String>(
230                                Arrays.asList(mergedConfig.getPropertySourceProperties()));
231                String differentiator = getDifferentiatorPropertySourceProperty();
232                if (differentiator != null) {
233                        propertySourceProperties.add(differentiator);
234                }
235                processPropertySourceProperties(mergedConfig, propertySourceProperties);
236                return propertySourceProperties;
237        }
238
239        /**
240         * Return a "differentiator" property to ensure that there is something to
241         * differentiate regular tests and bootstrapped tests. Without this property a cached
242         * context could be returned that wasn't created by this bootstrapper. By default uses
243         * the bootstrapper class as a property.
244         * @return the differentiator or {@code null}
245         */
246        protected String getDifferentiatorPropertySourceProperty() {
247                return getClass().getName() + "=true";
248        }
249
250        /**
251         * Post process the property source properties, adding or removing elements as
252         * required.
253         * @param mergedConfig the merged context configuration
254         * @param propertySourceProperties the property source properties to process
255         */
256        protected void processPropertySourceProperties(
257                        MergedContextConfiguration mergedConfig,
258                        List<String> propertySourceProperties) {
259                Class<?> testClass = mergedConfig.getTestClass();
260                String[] properties = getProperties(testClass);
261                if (!ObjectUtils.isEmpty(properties)) {
262                        // Added first so that inlined properties from @TestPropertySource take
263                        // precedence
264                        propertySourceProperties.addAll(0, Arrays.asList(properties));
265                }
266                if (getWebEnvironment(testClass) == WebEnvironment.RANDOM_PORT) {
267                        propertySourceProperties.add("server.port=0");
268                }
269        }
270
271        /**
272         * Return the {@link WebEnvironment} type for this test or null if undefined.
273         * @param testClass the source test class
274         * @return the {@link WebEnvironment} or {@code null}
275         */
276        protected WebEnvironment getWebEnvironment(Class<?> testClass) {
277                SpringBootTest annotation = getAnnotation(testClass);
278                return (annotation == null ? null : annotation.webEnvironment());
279        }
280
281        protected Class<?>[] getClasses(Class<?> testClass) {
282                SpringBootTest annotation = getAnnotation(testClass);
283                return (annotation == null ? null : annotation.classes());
284        }
285
286        protected String[] getProperties(Class<?> testClass) {
287                SpringBootTest annotation = getAnnotation(testClass);
288                return (annotation == null ? null : annotation.properties());
289        }
290
291        protected SpringBootTest getAnnotation(Class<?> testClass) {
292                return AnnotatedElementUtils.getMergedAnnotation(testClass, SpringBootTest.class);
293        }
294
295        protected void verifyConfiguration(Class<?> testClass) {
296                SpringBootTest springBootTest = getAnnotation(testClass);
297                if (springBootTest != null
298                                && (springBootTest.webEnvironment() == WebEnvironment.DEFINED_PORT
299                                                || springBootTest.webEnvironment() == WebEnvironment.RANDOM_PORT)
300                                && getAnnotation(WebAppConfiguration.class, testClass) != null) {
301                        throw new IllegalStateException("@WebAppConfiguration should only be used "
302                                        + "with @SpringBootTest when @SpringBootTest is configured with a "
303                                        + "mock web environment. Please remove @WebAppConfiguration or "
304                                        + "reconfigure @SpringBootTest.");
305                }
306        }
307
308        private <T extends Annotation> T getAnnotation(Class<T> annotationType,
309                        Class<?> testClass) {
310                return AnnotatedElementUtils.getMergedAnnotation(testClass, annotationType);
311        }
312
313        /**
314         * Create a new {@link MergedContextConfiguration} with different classes.
315         * @param mergedConfig the source config
316         * @param classes the replacement classes
317         * @return a new {@link MergedContextConfiguration}
318         */
319        protected final MergedContextConfiguration createModifiedConfig(
320                        MergedContextConfiguration mergedConfig, Class<?>[] classes) {
321                return createModifiedConfig(mergedConfig, classes,
322                                mergedConfig.getPropertySourceProperties());
323        }
324
325        /**
326         * Create a new {@link MergedContextConfiguration} with different classes and
327         * properties.
328         * @param mergedConfig the source config
329         * @param classes the replacement classes
330         * @param propertySourceProperties the replacement properties
331         * @return a new {@link MergedContextConfiguration}
332         */
333        protected final MergedContextConfiguration createModifiedConfig(
334                        MergedContextConfiguration mergedConfig, Class<?>[] classes,
335                        String[] propertySourceProperties) {
336                return new MergedContextConfiguration(mergedConfig.getTestClass(),
337                                mergedConfig.getLocations(), classes,
338                                mergedConfig.getContextInitializerClasses(),
339                                mergedConfig.getActiveProfiles(),
340                                mergedConfig.getPropertySourceLocations(), propertySourceProperties,
341                                mergedConfig.getContextCustomizers(), mergedConfig.getContextLoader(),
342                                getCacheAwareContextLoaderDelegate(), mergedConfig.getParent());
343        }
344
345}