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