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.util.ArrayList; 020import java.util.Arrays; 021import java.util.List; 022 023import org.springframework.beans.BeanUtils; 024import org.springframework.boot.SpringApplication; 025import org.springframework.boot.WebApplicationType; 026import org.springframework.boot.context.properties.bind.Bindable; 027import org.springframework.boot.context.properties.bind.Binder; 028import org.springframework.boot.context.properties.source.ConfigurationPropertySource; 029import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; 030import org.springframework.boot.test.mock.web.SpringBootMockServletContext; 031import org.springframework.boot.test.util.TestPropertyValues; 032import org.springframework.boot.web.reactive.context.GenericReactiveWebApplicationContext; 033import org.springframework.boot.web.servlet.support.ServletContextApplicationContextInitializer; 034import org.springframework.context.ApplicationContext; 035import org.springframework.context.ApplicationContextInitializer; 036import org.springframework.context.ConfigurableApplicationContext; 037import org.springframework.core.Ordered; 038import org.springframework.core.SpringVersion; 039import org.springframework.core.annotation.AnnotatedElementUtils; 040import org.springframework.core.annotation.Order; 041import org.springframework.core.env.ConfigurableEnvironment; 042import org.springframework.core.env.StandardEnvironment; 043import org.springframework.core.io.DefaultResourceLoader; 044import org.springframework.core.io.ResourceLoader; 045import org.springframework.test.context.ContextConfigurationAttributes; 046import org.springframework.test.context.ContextCustomizer; 047import org.springframework.test.context.ContextLoader; 048import org.springframework.test.context.MergedContextConfiguration; 049import org.springframework.test.context.support.AbstractContextLoader; 050import org.springframework.test.context.support.AnnotationConfigContextLoaderUtils; 051import org.springframework.test.context.support.TestPropertySourceUtils; 052import org.springframework.test.context.web.WebMergedContextConfiguration; 053import org.springframework.util.Assert; 054import org.springframework.util.ObjectUtils; 055import org.springframework.util.StringUtils; 056import org.springframework.web.context.support.GenericWebApplicationContext; 057 058/** 059 * A {@link ContextLoader} that can be used to test Spring Boot applications (those that 060 * normally startup using {@link SpringApplication}). Although this loader can be used 061 * directly, most test will instead want to use it with {@link SpringBootTest}. 062 * <p> 063 * The loader supports both standard {@link MergedContextConfiguration} as well as 064 * {@link WebMergedContextConfiguration}. If {@link WebMergedContextConfiguration} is used 065 * the context will either use a mock servlet environment, or start the full embedded web 066 * server. 067 * <p> 068 * If {@code @ActiveProfiles} are provided in the test class they will be used to create 069 * the application context. 070 * 071 * @author Dave Syer 072 * @author Phillip Webb 073 * @author Andy Wilkinson 074 * @author Stephane Nicoll 075 * @author Madhura Bhave 076 * @see SpringBootTest 077 */ 078public class SpringBootContextLoader extends AbstractContextLoader { 079 080 @Override 081 public ApplicationContext loadContext(MergedContextConfiguration config) 082 throws Exception { 083 Class<?>[] configClasses = config.getClasses(); 084 String[] configLocations = config.getLocations(); 085 Assert.state( 086 !ObjectUtils.isEmpty(configClasses) 087 || !ObjectUtils.isEmpty(configLocations), 088 () -> "No configuration classes " 089 + "or locations found in @SpringApplicationConfiguration. " 090 + "For default configuration detection to work you need " 091 + "Spring 4.0.3 or better (found " + SpringVersion.getVersion() 092 + ")."); 093 SpringApplication application = getSpringApplication(); 094 application.setMainApplicationClass(config.getTestClass()); 095 application.addPrimarySources(Arrays.asList(configClasses)); 096 application.getSources().addAll(Arrays.asList(configLocations)); 097 ConfigurableEnvironment environment = getEnvironment(); 098 if (!ObjectUtils.isEmpty(config.getActiveProfiles())) { 099 setActiveProfiles(environment, config.getActiveProfiles()); 100 } 101 ResourceLoader resourceLoader = (application.getResourceLoader() != null) 102 ? application.getResourceLoader() 103 : new DefaultResourceLoader(getClass().getClassLoader()); 104 TestPropertySourceUtils.addPropertiesFilesToEnvironment(environment, 105 resourceLoader, config.getPropertySourceLocations()); 106 TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, 107 getInlinedProperties(config)); 108 application.setEnvironment(environment); 109 List<ApplicationContextInitializer<?>> initializers = getInitializers(config, 110 application); 111 if (config instanceof WebMergedContextConfiguration) { 112 application.setWebApplicationType(WebApplicationType.SERVLET); 113 if (!isEmbeddedWebEnvironment(config)) { 114 new WebConfigurer().configure(config, application, initializers); 115 } 116 } 117 else if (config instanceof ReactiveWebMergedContextConfiguration) { 118 application.setWebApplicationType(WebApplicationType.REACTIVE); 119 if (!isEmbeddedWebEnvironment(config)) { 120 new ReactiveWebConfigurer().configure(application); 121 } 122 } 123 else { 124 application.setWebApplicationType(WebApplicationType.NONE); 125 } 126 application.setInitializers(initializers); 127 return application.run(); 128 } 129 130 /** 131 * Builds new {@link org.springframework.boot.SpringApplication} instance. You can 132 * override this method to add custom behavior 133 * @return {@link org.springframework.boot.SpringApplication} instance 134 */ 135 protected SpringApplication getSpringApplication() { 136 return new SpringApplication(); 137 } 138 139 /** 140 * Builds a new {@link ConfigurableEnvironment} instance. You can override this method 141 * to return something other than {@link StandardEnvironment} if necessary. 142 * @return a {@link ConfigurableEnvironment} instance 143 */ 144 protected ConfigurableEnvironment getEnvironment() { 145 return new StandardEnvironment(); 146 } 147 148 private void setActiveProfiles(ConfigurableEnvironment environment, 149 String[] profiles) { 150 TestPropertyValues 151 .of("spring.profiles.active=" 152 + StringUtils.arrayToCommaDelimitedString(profiles)) 153 .applyTo(environment); 154 } 155 156 protected String[] getInlinedProperties(MergedContextConfiguration config) { 157 ArrayList<String> properties = new ArrayList<>(); 158 // JMX bean names will clash if the same bean is used in multiple contexts 159 disableJmx(properties); 160 properties.addAll(Arrays.asList(config.getPropertySourceProperties())); 161 if (!isEmbeddedWebEnvironment(config) && !hasCustomServerPort(properties)) { 162 properties.add("server.port=-1"); 163 } 164 return StringUtils.toStringArray(properties); 165 } 166 167 private void disableJmx(List<String> properties) { 168 properties.add("spring.jmx.enabled=false"); 169 } 170 171 private boolean hasCustomServerPort(List<String> properties) { 172 Binder binder = new Binder(convertToConfigurationPropertySource(properties)); 173 return binder.bind("server.port", Bindable.of(String.class)).isBound(); 174 } 175 176 private ConfigurationPropertySource convertToConfigurationPropertySource( 177 List<String> properties) { 178 return new MapConfigurationPropertySource(TestPropertySourceUtils 179 .convertInlinedPropertiesToMap(StringUtils.toStringArray(properties))); 180 } 181 182 /** 183 * Return the {@link ApplicationContextInitializer initializers} that will be applied 184 * to the context. By default this method will adapt {@link ContextCustomizer context 185 * customizers}, add {@link SpringApplication#getInitializers() application 186 * initializers} and add 187 * {@link MergedContextConfiguration#getContextInitializerClasses() initializers 188 * specified on the test}. 189 * @param config the source context configuration 190 * @param application the application instance 191 * @return the initializers to apply 192 * @since 2.0.0 193 */ 194 protected List<ApplicationContextInitializer<?>> getInitializers( 195 MergedContextConfiguration config, SpringApplication application) { 196 List<ApplicationContextInitializer<?>> initializers = new ArrayList<>(); 197 for (ContextCustomizer contextCustomizer : config.getContextCustomizers()) { 198 initializers.add(new ContextCustomizerAdapter(contextCustomizer, config)); 199 } 200 initializers.addAll(application.getInitializers()); 201 for (Class<? extends ApplicationContextInitializer<?>> initializerClass : config 202 .getContextInitializerClasses()) { 203 initializers.add(BeanUtils.instantiateClass(initializerClass)); 204 } 205 if (config.getParent() != null) { 206 initializers.add(new ParentContextApplicationContextInitializer( 207 config.getParentApplicationContext())); 208 } 209 return initializers; 210 } 211 212 private boolean isEmbeddedWebEnvironment(MergedContextConfiguration config) { 213 SpringBootTest annotation = AnnotatedElementUtils 214 .findMergedAnnotation(config.getTestClass(), SpringBootTest.class); 215 if (annotation != null && annotation.webEnvironment().isEmbedded()) { 216 return true; 217 } 218 return false; 219 } 220 221 @Override 222 public void processContextConfiguration( 223 ContextConfigurationAttributes configAttributes) { 224 super.processContextConfiguration(configAttributes); 225 if (!configAttributes.hasResources()) { 226 Class<?>[] defaultConfigClasses = detectDefaultConfigurationClasses( 227 configAttributes.getDeclaringClass()); 228 configAttributes.setClasses(defaultConfigClasses); 229 } 230 } 231 232 /** 233 * Detect the default configuration classes for the supplied test class. By default 234 * simply delegates to 235 * {@link AnnotationConfigContextLoaderUtils#detectDefaultConfigurationClasses}. 236 * @param declaringClass the test class that declared {@code @ContextConfiguration} 237 * @return an array of default configuration classes, potentially empty but never 238 * {@code null} 239 * @see AnnotationConfigContextLoaderUtils 240 */ 241 protected Class<?>[] detectDefaultConfigurationClasses(Class<?> declaringClass) { 242 return AnnotationConfigContextLoaderUtils 243 .detectDefaultConfigurationClasses(declaringClass); 244 } 245 246 @Override 247 public ApplicationContext loadContext(String... locations) throws Exception { 248 throw new UnsupportedOperationException("SpringApplicationContextLoader " 249 + "does not support the loadContext(String...) method"); 250 } 251 252 @Override 253 protected String[] getResourceSuffixes() { 254 return new String[] { "-context.xml", "Context.groovy" }; 255 } 256 257 @Override 258 protected String getResourceSuffix() { 259 throw new IllegalStateException(); 260 } 261 262 /** 263 * Inner class to configure {@link WebMergedContextConfiguration}. 264 */ 265 private static class WebConfigurer { 266 267 private static final Class<GenericWebApplicationContext> WEB_CONTEXT_CLASS = GenericWebApplicationContext.class; 268 269 void configure(MergedContextConfiguration configuration, 270 SpringApplication application, 271 List<ApplicationContextInitializer<?>> initializers) { 272 WebMergedContextConfiguration webConfiguration = (WebMergedContextConfiguration) configuration; 273 addMockServletContext(initializers, webConfiguration); 274 application.setApplicationContextClass(WEB_CONTEXT_CLASS); 275 } 276 277 private void addMockServletContext( 278 List<ApplicationContextInitializer<?>> initializers, 279 WebMergedContextConfiguration webConfiguration) { 280 SpringBootMockServletContext servletContext = new SpringBootMockServletContext( 281 webConfiguration.getResourceBasePath()); 282 initializers.add(0, new ServletContextApplicationContextInitializer( 283 servletContext, true)); 284 } 285 286 } 287 288 /** 289 * Inner class to configure {@link ReactiveWebMergedContextConfiguration}. 290 */ 291 private static class ReactiveWebConfigurer { 292 293 private static final Class<GenericReactiveWebApplicationContext> WEB_CONTEXT_CLASS = GenericReactiveWebApplicationContext.class; 294 295 void configure(SpringApplication application) { 296 application.setApplicationContextClass(WEB_CONTEXT_CLASS); 297 } 298 299 } 300 301 /** 302 * Adapts a {@link ContextCustomizer} to a {@link ApplicationContextInitializer} so 303 * that it can be triggered via {@link SpringApplication}. 304 */ 305 private static class ContextCustomizerAdapter 306 implements ApplicationContextInitializer<ConfigurableApplicationContext> { 307 308 private final ContextCustomizer contextCustomizer; 309 310 private final MergedContextConfiguration config; 311 312 ContextCustomizerAdapter(ContextCustomizer contextCustomizer, 313 MergedContextConfiguration config) { 314 this.contextCustomizer = contextCustomizer; 315 this.config = config; 316 } 317 318 @Override 319 public void initialize(ConfigurableApplicationContext applicationContext) { 320 this.contextCustomizer.customizeContext(applicationContext, this.config); 321 } 322 323 } 324 325 @Order(Ordered.HIGHEST_PRECEDENCE) 326 private static class ParentContextApplicationContextInitializer 327 implements ApplicationContextInitializer<ConfigurableApplicationContext> { 328 329 private final ApplicationContext parent; 330 331 ParentContextApplicationContextInitializer(ApplicationContext parent) { 332 this.parent = parent; 333 } 334 335 @Override 336 public void initialize(ConfigurableApplicationContext applicationContext) { 337 applicationContext.setParent(this.parent); 338 } 339 340 } 341 342}