From 978cf8c2e61d846dc43ba98a268a610cde471972 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 7 Jan 2015 14:01:08 -0800 Subject: [PATCH] Polish @WebIntegrationTest support See gh-2299 --- .../tomcat/SampleTomcatApplicationTests.java | 8 +- .../boot/test/IntegrationTest.java | 4 +- .../MergedContextConfigurationProperties.java | 3 +- .../test/SpringApplicationContextLoader.java | 157 ++++++++++-------- ...AppIntegrationTestContextBootstrapper.java | 27 +-- .../boot/test/WebIntegrationTest.java | 9 + 6 files changed, 109 insertions(+), 99 deletions(-) diff --git a/spring-boot-samples/spring-boot-sample-tomcat/src/test/java/sample/tomcat/SampleTomcatApplicationTests.java b/spring-boot-samples/spring-boot-sample-tomcat/src/test/java/sample/tomcat/SampleTomcatApplicationTests.java index 9a77b9d51eb..f24ec1536ff 100644 --- a/spring-boot-samples/spring-boot-sample-tomcat/src/test/java/sample/tomcat/SampleTomcatApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-tomcat/src/test/java/sample/tomcat/SampleTomcatApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * Copyright 2012-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,14 +19,13 @@ package sample.tomcat; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.IntegrationTest; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.boot.test.TestRestTemplate; +import org.springframework.boot.test.WebIntegrationTest; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; import static org.junit.Assert.assertEquals; @@ -37,8 +36,7 @@ import static org.junit.Assert.assertEquals; */ @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = SampleTomcatApplication.class) -@WebAppConfiguration -@IntegrationTest("server.port:0") +@WebIntegrationTest(randomPort = true) @DirtiesContext public class SampleTomcatApplicationTests { diff --git a/spring-boot/src/main/java/org/springframework/boot/test/IntegrationTest.java b/spring-boot/src/main/java/org/springframework/boot/test/IntegrationTest.java index f7fc2431125..2a2d14c4783 100644 --- a/spring-boot/src/main/java/org/springframework/boot/test/IntegrationTest.java +++ b/spring-boot/src/main/java/org/springframework/boot/test/IntegrationTest.java @@ -32,8 +32,8 @@ import org.springframework.test.context.transaction.TransactionalTestExecutionLi /** * Test class annotation signifying that the tests are "integration tests" and therefore - * require full startup in the same way as a production application (listening on normal - * ports). Normally used in conjunction with {@code @SpringApplicationConfiguration}. + * require full startup in the same way as a production application. Normally used in + * conjunction with {@code @SpringApplicationConfiguration}. *

* If your test also uses {@code @WebAppConfiguration} consider using the * {@link WebIntegrationTest} instead. diff --git a/spring-boot/src/main/java/org/springframework/boot/test/MergedContextConfigurationProperties.java b/spring-boot/src/main/java/org/springframework/boot/test/MergedContextConfigurationProperties.java index 7494ab1ac41..6a7475e8706 100644 --- a/spring-boot/src/main/java/org/springframework/boot/test/MergedContextConfigurationProperties.java +++ b/spring-boot/src/main/java/org/springframework/boot/test/MergedContextConfigurationProperties.java @@ -37,10 +37,11 @@ class MergedContextConfigurationProperties { this.configuration = configuration; } - public void add(String[] properties) { + public void add(String[] properties, String... additional) { Set merged = new LinkedHashSet((Arrays.asList(this.configuration .getPropertySourceProperties()))); merged.addAll(Arrays.asList(properties)); + merged.addAll(Arrays.asList(additional)); addIntegrationTestProperty(merged); ReflectionTestUtils.setField(this.configuration, "propertySourceProperties", merged.toArray(new String[merged.size()])); diff --git a/spring-boot/src/main/java/org/springframework/boot/test/SpringApplicationContextLoader.java b/spring-boot/src/main/java/org/springframework/boot/test/SpringApplicationContextLoader.java index c827e5365d2..ca6f900f83f 100644 --- a/spring-boot/src/main/java/org/springframework/boot/test/SpringApplicationContextLoader.java +++ b/spring-boot/src/main/java/org/springframework/boot/test/SpringApplicationContextLoader.java @@ -46,30 +46,28 @@ import org.springframework.test.context.support.AbstractContextLoader; import org.springframework.test.context.support.AnnotationConfigContextLoaderUtils; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.context.web.WebMergedContextConfiguration; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.context.support.GenericWebApplicationContext; /** * A {@link ContextLoader} that can be used to test Spring Boot applications (those that - * normally startup using {@link SpringApplication}). Normally never starts an embedded - * web server, but detects the {@link WebAppConfiguration @WebAppConfiguration} annotation - * on the test class and only creates a web application context if it is present. Non-web - * features, like a repository layer, can be tested cleanly by simply not marking - * the test class @WebAppConfiguration. + * normally startup using {@link SpringApplication}). Can be used to test non-web features + * (like a repository layer) or start an fully-configured embedded servlet container. *

- * If you want to start a web server, mark the test class as - * @WebAppConfiguration @IntegrationTest. This is useful for testing HTTP - * endpoints using {@link TestRestTemplate} (for instance), especially since you can - * @Autowired application context components into your test case to see the - * internal effects of HTTP requests directly. + * Use {@code @WebIntegrationTest} (or {@code @IntegrationTest} with + * {@code @WebAppConfiguration}) to indicate that you want to use a real servlet container + * or {@code @WebAppConfiguration} alone to use a {@link MockServletContext}. *

* If @ActiveProfiles are provided in the test class they will be used to * create the application context. * * @author Dave Syer + * @author Phillip Webb * @see IntegrationTest * @see WebIntegrationTest + * @see TestRestTemplate */ public class SpringApplicationContextLoader extends AbstractContextLoader { @@ -78,21 +76,15 @@ public class SpringApplicationContextLoader extends AbstractContextLoader { @Override public ApplicationContext loadContext(MergedContextConfiguration config) throws Exception { + assertValidAnnotations(config.getTestClass()); SpringApplication application = getSpringApplication(); application.setSources(getSources(config)); ConfigurableEnvironment environment = new StandardEnvironment(); if (!ObjectUtils.isEmpty(config.getActiveProfiles())) { - String profiles = StringUtils.arrayToCommaDelimitedString(config - .getActiveProfiles()); - EnvironmentTestUtils.addEnvironment(environment, "spring.profiles.active=" - + profiles); + setActiveProfiles(environment, config.getActiveProfiles()); } - // Ensure @IntegrationTest properties go before external config and after system - environment.getPropertySources() - .addAfter( - StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, - new MapPropertySource("integrationTest", - getEnvironmentProperties(config))); + Map properties = getEnvironmentProperties(config); + addProperties(environment, properties); application.setEnvironment(environment); List> initializers = getInitializers(config, application); @@ -106,13 +98,14 @@ public class SpringApplicationContextLoader extends AbstractContextLoader { return application.run(); } - @Override - public void processContextConfiguration( - ContextConfigurationAttributes configAttributes) { - if (!configAttributes.hasLocations() && !configAttributes.hasClasses()) { - Class[] defaultConfigClasses = detectDefaultConfigurationClasses(configAttributes - .getDeclaringClass()); - configAttributes.setClasses(defaultConfigClasses); + private void assertValidAnnotations(Class testClass) { + boolean hasWebAppConfiguration = AnnotationUtils.findAnnotation(testClass, + WebAppConfiguration.class) != null; + boolean hasWebIntegrationTest = AnnotationUtils.findAnnotation(testClass, + WebIntegrationTest.class) != null; + if (hasWebAppConfiguration && hasWebIntegrationTest) { + throw new IllegalStateException("@WebIntegrationTest and " + + "@WebAppConfiguration cannot be used together"); } } @@ -129,27 +122,16 @@ public class SpringApplicationContextLoader extends AbstractContextLoader { Set sources = new LinkedHashSet(); sources.addAll(Arrays.asList(mergedConfig.getClasses())); sources.addAll(Arrays.asList(mergedConfig.getLocations())); - if (sources.isEmpty()) { - throw new IllegalStateException( - "No configuration classes or locations found in @SpringApplicationConfiguration. " - + "For default configuration detection to work you need Spring 4.0.3 or better (found " - + SpringVersion.getVersion() + ")."); - } + Assert.state(sources.size() > 0, "No configuration classes " + + "or locations found in @SpringApplicationConfiguration. " + + "For default configuration detection to work you need " + + "Spring 4.0.3 or better (found " + SpringVersion.getVersion() + ")."); return sources; } - /** - * Detect the default configuration classes for the supplied test class. By default - * simply delegates to - * {@link AnnotationConfigContextLoaderUtils#detectDefaultConfigurationClasses} . - * @param declaringClass the test class that declared {@code @ContextConfiguration} - * @return an array of default configuration classes, potentially empty but never - * {@code null} - * @see AnnotationConfigContextLoaderUtils - */ - protected Class[] detectDefaultConfigurationClasses(Class declaringClass) { - return AnnotationConfigContextLoaderUtils - .detectDefaultConfigurationClasses(declaringClass); + private void setActiveProfiles(ConfigurableEnvironment environment, String[] profiles) { + EnvironmentTestUtils.addEnvironment(environment, "spring.profiles.active=" + + StringUtils.arrayToCommaDelimitedString(profiles)); } protected Map getEnvironmentProperties( @@ -159,8 +141,7 @@ public class SpringApplicationContextLoader extends AbstractContextLoader { disableJmx(properties); properties.putAll(extractEnvironmentProperties(config .getPropertySourceProperties())); - if (!isAnnotated(config.getTestClass(), IntegrationTest.class, - WebIntegrationTest.class)) { + if (!isIntegrationTest(config.getTestClass())) { properties.putAll(getDefaultEnvironmentProperties()); } return properties; @@ -170,11 +151,7 @@ public class SpringApplicationContextLoader extends AbstractContextLoader { properties.put("spring.jmx.enabled", "false"); } - private Map getDefaultEnvironmentProperties() { - return Collections.singletonMap("server.port", "-1"); - } - - Map extractEnvironmentProperties(String[] values) { + final Map extractEnvironmentProperties(String[] values) { // Instead of parsing the keys ourselves, we rely on standard handling if (values == null) { return Collections.emptyMap(); @@ -199,6 +176,18 @@ public class SpringApplicationContextLoader extends AbstractContextLoader { return map; } + private Map getDefaultEnvironmentProperties() { + return Collections.singletonMap("server.port", "-1"); + } + + private void addProperties(ConfigurableEnvironment environment, + Map properties) { + // @IntegrationTest properties go before external configuration and after system + environment.getPropertySources().addAfter( + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, + new MapPropertySource("integrationTest", properties)); + } + private List> getInitializers( MergedContextConfiguration mergedConfig, SpringApplication application) { List> initializers = new ArrayList>(); @@ -211,10 +200,34 @@ public class SpringApplicationContextLoader extends AbstractContextLoader { return initializers; } + @Override + public void processContextConfiguration( + ContextConfigurationAttributes configAttributes) { + if (!configAttributes.hasLocations() && !configAttributes.hasClasses()) { + Class[] defaultConfigClasses = detectDefaultConfigurationClasses(configAttributes + .getDeclaringClass()); + configAttributes.setClasses(defaultConfigClasses); + } + } + + /** + * Detect the default configuration classes for the supplied test class. By default + * simply delegates to + * {@link AnnotationConfigContextLoaderUtils#detectDefaultConfigurationClasses} . + * @param declaringClass the test class that declared {@code @ContextConfiguration} + * @return an array of default configuration classes, potentially empty but never + * {@code null} + * @see AnnotationConfigContextLoaderUtils + */ + protected Class[] detectDefaultConfigurationClasses(Class declaringClass) { + return AnnotationConfigContextLoaderUtils + .detectDefaultConfigurationClasses(declaringClass); + } + @Override public ApplicationContext loadContext(String... locations) throws Exception { - throw new UnsupportedOperationException( - "SpringApplicationContextLoader does not support the loadContext(String...) method"); + throw new UnsupportedOperationException("SpringApplicationContextLoader " + + "does not support the loadContext(String...) method"); } @Override @@ -222,33 +235,37 @@ public class SpringApplicationContextLoader extends AbstractContextLoader { return "-context.xml"; } + /** + * Inner class to configure {@link WebMergedContextConfiguration}. + */ private static class WebConfigurer { + private static final Class WEB_CONTEXT_CLASS = GenericWebApplicationContext.class; + void configure(MergedContextConfiguration configuration, SpringApplication application, List> initializers) { WebMergedContextConfiguration webConfiguration = (WebMergedContextConfiguration) configuration; - if (!isAnnotated(webConfiguration.getTestClass(), IntegrationTest.class, - WebIntegrationTest.class)) { - MockServletContext servletContext = new MockServletContext( - webConfiguration.getResourceBasePath()); - initializers.add(0, new ServletContextApplicationContextInitializer( - servletContext)); - application - .setApplicationContextClass(GenericWebApplicationContext.class); + if (!isIntegrationTest(webConfiguration.getTestClass())) { + addMockServletContext(initializers, webConfiguration); + application.setApplicationContextClass(WEB_CONTEXT_CLASS); } } + private void addMockServletContext( + List> initializers, + WebMergedContextConfiguration webConfiguration) { + MockServletContext servletContext = new MockServletContext( + webConfiguration.getResourceBasePath()); + initializers.add(0, new ServletContextApplicationContextInitializer( + servletContext)); + } + } - @SuppressWarnings({ "unchecked", "rawtypes" }) - private static boolean isAnnotated(Class testClass, Class... annotations) { - for (Class annotation : annotations) { - if (AnnotationUtils.findAnnotation(testClass, (Class) annotation) != null) { - return true; - } - } - return false; + private static boolean isIntegrationTest(Class testClass) { + return ((AnnotationUtils.findAnnotation(testClass, IntegrationTest.class) != null) || (AnnotationUtils + .findAnnotation(testClass, WebIntegrationTest.class) != null)); } } diff --git a/spring-boot/src/main/java/org/springframework/boot/test/WebAppIntegrationTestContextBootstrapper.java b/spring-boot/src/main/java/org/springframework/boot/test/WebAppIntegrationTestContextBootstrapper.java index 4bced26e574..73db30947d9 100644 --- a/spring-boot/src/main/java/org/springframework/boot/test/WebAppIntegrationTestContextBootstrapper.java +++ b/spring-boot/src/main/java/org/springframework/boot/test/WebAppIntegrationTestContextBootstrapper.java @@ -16,16 +16,11 @@ package org.springframework.boot.test; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - import org.springframework.core.annotation.AnnotationUtils; import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.TestContextBootstrapper; import org.springframework.test.context.support.DefaultTestContextBootstrapper; -import org.springframework.test.context.web.ServletTestExecutionListener; import org.springframework.test.context.web.WebDelegatingSmartContextLoader; import org.springframework.test.context.web.WebMergedContextConfiguration; @@ -55,24 +50,14 @@ class WebAppIntegrationTestContextBootstrapper extends DefaultTestContextBootstr mergedConfig = new WebMergedContextConfiguration(mergedConfig, null); MergedContextConfigurationProperties properties = new MergedContextConfigurationProperties( mergedConfig); - properties.add(annotation.value()); + if (annotation.randomPort()) { + properties.add(annotation.value(), "server.port:0"); + } + else { + properties.add(annotation.value()); + } } return mergedConfig; } - @Override - protected List getDefaultTestExecutionListenerClassNames() { - WebIntegrationTest annotation = AnnotationUtils.findAnnotation( - getBootstrapContext().getTestClass(), WebIntegrationTest.class); - List listeners = super.getDefaultTestExecutionListenerClassNames(); - if (annotation != null) { - // Leave out the ServletTestExecutionListener because it only deals with - // Mock* servlet stuff. A real embedded application will not need the mocks. - listeners = new ArrayList(listeners); - listeners.remove(ServletTestExecutionListener.class.getName()); - listeners.add(IntegrationTestPropertiesListener.class.getName()); - } - return Collections.unmodifiableList(listeners); - } - } diff --git a/spring-boot/src/main/java/org/springframework/boot/test/WebIntegrationTest.java b/spring-boot/src/main/java/org/springframework/boot/test/WebIntegrationTest.java index 58c6e582547..fe5e7b8d0ca 100644 --- a/spring-boot/src/main/java/org/springframework/boot/test/WebIntegrationTest.java +++ b/spring-boot/src/main/java/org/springframework/boot/test/WebIntegrationTest.java @@ -37,6 +37,7 @@ import org.springframework.test.context.BootstrapWith; * * @author Phillip Webb * @since 1.2.1 + * @see IntegrationTest */ @Documented @Inherited @@ -51,4 +52,12 @@ public @interface WebIntegrationTest { */ String[] value() default {}; + /** + * Convenience attribute that can be used to set a {@code server.port=0} + * {@link Environment} property which usually triggers listening on a random port. + * Often used in conjunction with a {@code @Value("server.local.port")} injected field + * on the test. + */ + boolean randomPort() default false; + }