001/* 002 * Copyright 2012-2018 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.WebApplicationType; 031import org.springframework.boot.context.properties.bind.Bindable; 032import org.springframework.boot.context.properties.bind.Binder; 033import org.springframework.boot.context.properties.source.ConfigurationPropertySource; 034import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; 035import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 036import org.springframework.core.annotation.AnnotatedElementUtils; 037import org.springframework.core.annotation.AnnotationUtils; 038import org.springframework.core.env.Environment; 039import org.springframework.core.io.support.SpringFactoriesLoader; 040import org.springframework.test.context.ContextConfiguration; 041import org.springframework.test.context.ContextConfigurationAttributes; 042import org.springframework.test.context.ContextHierarchy; 043import org.springframework.test.context.ContextLoader; 044import org.springframework.test.context.MergedContextConfiguration; 045import org.springframework.test.context.TestContext; 046import org.springframework.test.context.TestContextBootstrapper; 047import org.springframework.test.context.TestExecutionListener; 048import org.springframework.test.context.support.DefaultTestContextBootstrapper; 049import org.springframework.test.context.support.TestPropertySourceUtils; 050import org.springframework.test.context.web.WebAppConfiguration; 051import org.springframework.test.context.web.WebMergedContextConfiguration; 052import org.springframework.util.Assert; 053import org.springframework.util.ClassUtils; 054import org.springframework.util.ObjectUtils; 055import org.springframework.util.StringUtils; 056 057/** 058 * {@link TestContextBootstrapper} for Spring Boot. Provides support for 059 * {@link SpringBootTest @SpringBootTest} and may also be used directly or subclassed. 060 * Provides the following features over and above {@link DefaultTestContextBootstrapper}: 061 * <ul> 062 * <li>Uses {@link SpringBootContextLoader} as the 063 * {@link #getDefaultContextLoaderClass(Class) default context loader}.</li> 064 * <li>Automatically searches for a 065 * {@link SpringBootConfiguration @SpringBootConfiguration} when required.</li> 066 * <li>Allows custom {@link Environment} {@link #getProperties(Class)} to be defined.</li> 067 * <li>Provides support for different {@link WebEnvironment webEnvironment} modes.</li> 068 * </ul> 069 * 070 * @author Phillip Webb 071 * @author Andy Wilkinson 072 * @author Brian Clozel 073 * @author Madhura Bhave 074 * @since 1.4.0 075 * @see SpringBootTest 076 * @see TestConfiguration 077 */ 078public class SpringBootTestContextBootstrapper extends DefaultTestContextBootstrapper { 079 080 private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet", 081 "org.springframework.web.context.ConfigurableWebApplicationContext" }; 082 083 private static final String REACTIVE_WEB_ENVIRONMENT_CLASS = "org.springframework." 084 + "web.reactive.DispatcherHandler"; 085 086 private static final String MVC_WEB_ENVIRONMENT_CLASS = "org.springframework." 087 + "web.servlet.DispatcherServlet"; 088 089 private static final String JERSEY_WEB_ENVIRONMENT_CLASS = "org.glassfish.jersey.server.ResourceConfig"; 090 091 private static final String ACTIVATE_SERVLET_LISTENER = "org.springframework.test." 092 + "context.web.ServletTestExecutionListener.activateListener"; 093 094 private static final Log logger = LogFactory 095 .getLog(SpringBootTestContextBootstrapper.class); 096 097 @Override 098 public TestContext buildTestContext() { 099 TestContext context = super.buildTestContext(); 100 verifyConfiguration(context.getTestClass()); 101 WebEnvironment webEnvironment = getWebEnvironment(context.getTestClass()); 102 if (webEnvironment == WebEnvironment.MOCK 103 && deduceWebApplicationType() == WebApplicationType.SERVLET) { 104 context.setAttribute(ACTIVATE_SERVLET_LISTENER, true); 105 } 106 else if (webEnvironment != null && webEnvironment.isEmbedded()) { 107 context.setAttribute(ACTIVATE_SERVLET_LISTENER, false); 108 } 109 return context; 110 } 111 112 @Override 113 protected Set<Class<? extends TestExecutionListener>> getDefaultTestExecutionListenerClasses() { 114 Set<Class<? extends TestExecutionListener>> listeners = super.getDefaultTestExecutionListenerClasses(); 115 List<DefaultTestExecutionListenersPostProcessor> postProcessors = SpringFactoriesLoader 116 .loadFactories(DefaultTestExecutionListenersPostProcessor.class, 117 getClass().getClassLoader()); 118 for (DefaultTestExecutionListenersPostProcessor postProcessor : postProcessors) { 119 listeners = postProcessor.postProcessDefaultTestExecutionListeners(listeners); 120 } 121 return listeners; 122 } 123 124 @Override 125 protected ContextLoader resolveContextLoader(Class<?> testClass, 126 List<ContextConfigurationAttributes> configAttributesList) { 127 Class<?>[] classes = getClasses(testClass); 128 if (!ObjectUtils.isEmpty(classes)) { 129 for (ContextConfigurationAttributes configAttributes : configAttributesList) { 130 addConfigAttributesClasses(configAttributes, classes); 131 } 132 } 133 return super.resolveContextLoader(testClass, configAttributesList); 134 } 135 136 private void addConfigAttributesClasses( 137 ContextConfigurationAttributes configAttributes, Class<?>[] classes) { 138 List<Class<?>> combined = new ArrayList<>(); 139 combined.addAll(Arrays.asList(classes)); 140 if (configAttributes.getClasses() != null) { 141 combined.addAll(Arrays.asList(configAttributes.getClasses())); 142 } 143 configAttributes.setClasses(ClassUtils.toClassArray(combined)); 144 } 145 146 @Override 147 protected Class<? extends ContextLoader> getDefaultContextLoaderClass( 148 Class<?> testClass) { 149 return SpringBootContextLoader.class; 150 } 151 152 @Override 153 protected MergedContextConfiguration processMergedContextConfiguration( 154 MergedContextConfiguration mergedConfig) { 155 Class<?>[] classes = getOrFindConfigurationClasses(mergedConfig); 156 List<String> propertySourceProperties = getAndProcessPropertySourceProperties( 157 mergedConfig); 158 mergedConfig = createModifiedConfig(mergedConfig, classes, 159 StringUtils.toStringArray(propertySourceProperties)); 160 WebEnvironment webEnvironment = getWebEnvironment(mergedConfig.getTestClass()); 161 if (webEnvironment != null && isWebEnvironmentSupported(mergedConfig)) { 162 WebApplicationType webApplicationType = getWebApplicationType(mergedConfig); 163 if (webApplicationType == WebApplicationType.SERVLET 164 && (webEnvironment.isEmbedded() 165 || webEnvironment == WebEnvironment.MOCK)) { 166 WebAppConfiguration webAppConfiguration = AnnotatedElementUtils 167 .findMergedAnnotation(mergedConfig.getTestClass(), 168 WebAppConfiguration.class); 169 String resourceBasePath = (webAppConfiguration != null) 170 ? webAppConfiguration.value() : "src/main/webapp"; 171 mergedConfig = new WebMergedContextConfiguration(mergedConfig, 172 resourceBasePath); 173 } 174 else if (webApplicationType == WebApplicationType.REACTIVE 175 && (webEnvironment.isEmbedded() 176 || webEnvironment == WebEnvironment.MOCK)) { 177 return new ReactiveWebMergedContextConfiguration(mergedConfig); 178 } 179 } 180 return mergedConfig; 181 } 182 183 private WebApplicationType getWebApplicationType( 184 MergedContextConfiguration configuration) { 185 ConfigurationPropertySource source = new MapConfigurationPropertySource( 186 TestPropertySourceUtils.convertInlinedPropertiesToMap( 187 configuration.getPropertySourceProperties())); 188 Binder binder = new Binder(source); 189 return binder 190 .bind("spring.main.web-application-type", 191 Bindable.of(WebApplicationType.class)) 192 .orElseGet(this::deduceWebApplicationType); 193 } 194 195 private WebApplicationType deduceWebApplicationType() { 196 if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null) 197 && !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null) 198 && !ClassUtils.isPresent(JERSEY_WEB_ENVIRONMENT_CLASS, null)) { 199 return WebApplicationType.REACTIVE; 200 } 201 for (String className : WEB_ENVIRONMENT_CLASSES) { 202 if (!ClassUtils.isPresent(className, null)) { 203 return WebApplicationType.NONE; 204 } 205 } 206 return WebApplicationType.SERVLET; 207 } 208 209 private boolean isWebEnvironmentSupported(MergedContextConfiguration mergedConfig) { 210 Class<?> testClass = mergedConfig.getTestClass(); 211 ContextHierarchy hierarchy = AnnotationUtils.getAnnotation(testClass, 212 ContextHierarchy.class); 213 if (hierarchy == null || hierarchy.value().length == 0) { 214 return true; 215 } 216 ContextConfiguration[] configurations = hierarchy.value(); 217 return isFromConfiguration(mergedConfig, 218 configurations[configurations.length - 1]); 219 } 220 221 private boolean isFromConfiguration(MergedContextConfiguration candidateConfig, 222 ContextConfiguration configuration) { 223 ContextConfigurationAttributes attributes = new ContextConfigurationAttributes( 224 candidateConfig.getTestClass(), configuration); 225 Set<Class<?>> configurationClasses = new HashSet<>( 226 Arrays.asList(attributes.getClasses())); 227 for (Class<?> candidate : candidateConfig.getClasses()) { 228 if (configurationClasses.contains(candidate)) { 229 return true; 230 } 231 } 232 return false; 233 } 234 235 protected Class<?>[] getOrFindConfigurationClasses( 236 MergedContextConfiguration mergedConfig) { 237 Class<?>[] classes = mergedConfig.getClasses(); 238 if (containsNonTestComponent(classes) || mergedConfig.hasLocations()) { 239 return classes; 240 } 241 Class<?> found = new AnnotatedClassFinder(SpringBootConfiguration.class) 242 .findFromClass(mergedConfig.getTestClass()); 243 Assert.state(found != null, 244 "Unable to find a @SpringBootConfiguration, you need to use " 245 + "@ContextConfiguration or @SpringBootTest(classes=...) " 246 + "with your test"); 247 logger.info("Found @SpringBootConfiguration " + found.getName() + " for test " 248 + mergedConfig.getTestClass()); 249 return merge(found, classes); 250 } 251 252 private boolean containsNonTestComponent(Class<?>[] classes) { 253 for (Class<?> candidate : classes) { 254 if (!AnnotatedElementUtils.isAnnotated(candidate, TestConfiguration.class)) { 255 return true; 256 } 257 } 258 return false; 259 } 260 261 private Class<?>[] merge(Class<?> head, Class<?>[] existing) { 262 Class<?>[] result = new Class<?>[existing.length + 1]; 263 result[0] = head; 264 System.arraycopy(existing, 0, result, 1, existing.length); 265 return result; 266 } 267 268 private List<String> getAndProcessPropertySourceProperties( 269 MergedContextConfiguration mergedConfig) { 270 List<String> propertySourceProperties = new ArrayList<>( 271 Arrays.asList(mergedConfig.getPropertySourceProperties())); 272 String differentiator = getDifferentiatorPropertySourceProperty(); 273 if (differentiator != null) { 274 propertySourceProperties.add(differentiator); 275 } 276 processPropertySourceProperties(mergedConfig, propertySourceProperties); 277 return propertySourceProperties; 278 } 279 280 /** 281 * Return a "differentiator" property to ensure that there is something to 282 * differentiate regular tests and bootstrapped tests. Without this property a cached 283 * context could be returned that wasn't created by this bootstrapper. By default uses 284 * the bootstrapper class as a property. 285 * @return the differentiator or {@code null} 286 */ 287 protected String getDifferentiatorPropertySourceProperty() { 288 return getClass().getName() + "=true"; 289 } 290 291 /** 292 * Post process the property source properties, adding or removing elements as 293 * required. 294 * @param mergedConfig the merged context configuration 295 * @param propertySourceProperties the property source properties to process 296 */ 297 protected void processPropertySourceProperties( 298 MergedContextConfiguration mergedConfig, 299 List<String> propertySourceProperties) { 300 Class<?> testClass = mergedConfig.getTestClass(); 301 String[] properties = getProperties(testClass); 302 if (!ObjectUtils.isEmpty(properties)) { 303 // Added first so that inlined properties from @TestPropertySource take 304 // precedence 305 propertySourceProperties.addAll(0, Arrays.asList(properties)); 306 } 307 if (getWebEnvironment(testClass) == WebEnvironment.RANDOM_PORT) { 308 propertySourceProperties.add("server.port=0"); 309 } 310 } 311 312 /** 313 * Return the {@link WebEnvironment} type for this test or null if undefined. 314 * @param testClass the source test class 315 * @return the {@link WebEnvironment} or {@code null} 316 */ 317 protected WebEnvironment getWebEnvironment(Class<?> testClass) { 318 SpringBootTest annotation = getAnnotation(testClass); 319 return (annotation != null) ? annotation.webEnvironment() : null; 320 } 321 322 protected Class<?>[] getClasses(Class<?> testClass) { 323 SpringBootTest annotation = getAnnotation(testClass); 324 return (annotation != null) ? annotation.classes() : null; 325 } 326 327 protected String[] getProperties(Class<?> testClass) { 328 SpringBootTest annotation = getAnnotation(testClass); 329 return (annotation != null) ? annotation.properties() : null; 330 } 331 332 protected SpringBootTest getAnnotation(Class<?> testClass) { 333 return AnnotatedElementUtils.getMergedAnnotation(testClass, SpringBootTest.class); 334 } 335 336 protected void verifyConfiguration(Class<?> testClass) { 337 SpringBootTest springBootTest = getAnnotation(testClass); 338 if (springBootTest != null 339 && (springBootTest.webEnvironment() == WebEnvironment.DEFINED_PORT 340 || springBootTest.webEnvironment() == WebEnvironment.RANDOM_PORT) 341 && getAnnotation(WebAppConfiguration.class, testClass) != null) { 342 throw new IllegalStateException("@WebAppConfiguration should only be used " 343 + "with @SpringBootTest when @SpringBootTest is configured with a " 344 + "mock web environment. Please remove @WebAppConfiguration or " 345 + "reconfigure @SpringBootTest."); 346 } 347 } 348 349 private <T extends Annotation> T getAnnotation(Class<T> annotationType, 350 Class<?> testClass) { 351 return AnnotatedElementUtils.getMergedAnnotation(testClass, annotationType); 352 } 353 354 /** 355 * Create a new {@link MergedContextConfiguration} with different classes. 356 * @param mergedConfig the source config 357 * @param classes the replacement classes 358 * @return a new {@link MergedContextConfiguration} 359 */ 360 protected final MergedContextConfiguration createModifiedConfig( 361 MergedContextConfiguration mergedConfig, Class<?>[] classes) { 362 return createModifiedConfig(mergedConfig, classes, 363 mergedConfig.getPropertySourceProperties()); 364 } 365 366 /** 367 * Create a new {@link MergedContextConfiguration} with different classes and 368 * properties. 369 * @param mergedConfig the source config 370 * @param classes the replacement classes 371 * @param propertySourceProperties the replacement properties 372 * @return a new {@link MergedContextConfiguration} 373 */ 374 protected final MergedContextConfiguration createModifiedConfig( 375 MergedContextConfiguration mergedConfig, Class<?>[] classes, 376 String[] propertySourceProperties) { 377 return new MergedContextConfiguration(mergedConfig.getTestClass(), 378 mergedConfig.getLocations(), classes, 379 mergedConfig.getContextInitializerClasses(), 380 mergedConfig.getActiveProfiles(), 381 mergedConfig.getPropertySourceLocations(), propertySourceProperties, 382 mergedConfig.getContextCustomizers(), mergedConfig.getContextLoader(), 383 getCacheAwareContextLoaderDelegate(), mergedConfig.getParent()); 384 } 385 386}