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}