From be30385e152ed7770a4a6959922117bdcf2524de Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 6 Jan 2015 19:03:29 -0800 Subject: [PATCH] Add @WebIntegrationTest annotation Add `@WebIntegrationTest` which is similar to `@IntegrationTest` and `@WebAppConfiguration`. The annotation using Spring's `@BootstrapWith` annotation rather than `@TestExecutionListeners` which allows it to work when `@TestExecutionListeners` (even ServletTestExecutionListener) are declared on the test class. This annotation is particularly useful for TestNG users that extend Spring's `AbstractTestNGSpringContextTests` class. Fixes gh-2299 See gh-1956 See gh-2135 --- spring-boot-docs/src/main/asciidoc/howto.adoc | 5 +- .../main/asciidoc/spring-boot-features.adoc | 18 ++-- .../boot/test/IntegrationTest.java | 8 +- .../IntegrationTestPropertiesListener.java | 42 ++------ .../MergedContextConfigurationProperties.java | 60 +++++++++++ .../test/SpringApplicationContextLoader.java | 20 +++- ...AppIntegrationTestContextBootstrapper.java | 78 ++++++++++++++ .../boot/test/WebIntegrationTest.java | 54 ++++++++++ ...ingApplicationWebIntegrationTestTests.java | 100 ++++++++++++++++++ 9 files changed, 332 insertions(+), 53 deletions(-) create mode 100644 spring-boot/src/main/java/org/springframework/boot/test/MergedContextConfigurationProperties.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/test/WebAppIntegrationTestContextBootstrapper.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/test/WebIntegrationTest.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/test/SpringApplicationWebIntegrationTestTests.java diff --git a/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-docs/src/main/asciidoc/howto.adoc index 227f05e70e..a4847b0a7b 100644 --- a/spring-boot-docs/src/main/asciidoc/howto.adoc +++ b/spring-boot-docs/src/main/asciidoc/howto.adoc @@ -365,15 +365,14 @@ that and be sure that it has initialized is to add a `@Bean` of type `ApplicationListener` and pull the container out of the event when it is published. -A useful practice for use with `@IntegrationTests` is to set `server.port=0` +A useful practice for use with `@WebIntegrationTests` is to set `server.port=0` and then inject the actual ('`local`') port as a `@Value`. For example: [source,java,indent=0,subs="verbatim,quotes,attributes"] ---- @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = SampleDataJpaApplication.class) - @WebAppConfiguration - @IntegrationTest("server.port:0") + @WebIntegrationTest("server.port:0") public class CityRepositoryIntegrationTests { @Autowired diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index c1eb7461ae..4c8ab7012b 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -2381,12 +2381,12 @@ For example: ---- TIP: The context loader guesses whether you want to test a web application or not (e.g. -with `MockMVC`) by looking for the `@WebAppConfiguration` annotation. (`MockMVC` and -`@WebAppConfiguration` are part of `spring-test`). +with `MockMVC`) by looking for the `@WebIntegrationTest` or `@WebAppConfiguration` +annotations. (`MockMVC` and `@WebAppConfiguration` are part of `spring-test`). If you want a web application to start up and listen on its normal port, so you can test it with HTTP (e.g. using `RestTemplate`), annotate your test class (or one of its -superclasses) with `@IntegrationTest`. This can be very useful because it means you can +superclasses) with `@WebIntegrationTest`. This can be very useful because it means you can test the full stack of your application, but also inject its components into the test class and use them to assert the internal state of the application after an HTTP interaction. For example: @@ -2395,8 +2395,7 @@ interaction. For example: ---- @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = SampleDataJpaApplication.class) - @WebAppConfiguration - @IntegrationTest + @WebIntegrationTest public class CityRepositoryIntegrationTests { @Autowired @@ -2414,8 +2413,8 @@ as long as your tests share the same configuration, the time consuming process o and stopping the server will only happen once, regardless of the number of tests that actually run. -To change the port you can add environment properties to `@IntegrationTest` as colon- or -equals-separated name-value pairs, e.g. `@IntegrationTest("server.port:9000")`. +To change the port you can add environment properties to `@WebIntegrationTest` as colon- +or equals-separated name-value pairs, e.g. `@WebIntegrationTest("server.port:9000")`. Additionally you can set the `server.port` and `management.port` properties to `0` in order to run your integration tests using random ports. For example: @@ -2423,8 +2422,7 @@ in order to run your integration tests using random ports. For example: ---- @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = MyApplication.class) - @WebAppConfiguration - @IntegrationTest({"server.port=0", "management.port=0"}) + @WebIntegrationTest({"server.port=0", "management.port=0"}) public class SomeIntegrationTests { // ... @@ -2462,7 +2460,7 @@ Boot specific context loader: NOTE: The annotations <> can be used with Spock, i.e. you can annotate your `Specification` with -`@IntegrationTest` and `@WebAppConfiguration` to suit the needs of your tests. +`@WebIntegrationTest` to suit the needs of your tests. 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 d16e3759be..f7fc243112 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 @@ -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. @@ -33,9 +33,13 @@ 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). + * ports). Normally used in conjunction with {@code @SpringApplicationConfiguration}. + *

+ * If your test also uses {@code @WebAppConfiguration} consider using the + * {@link WebIntegrationTest} instead. * * @author Dave Syer + * @see WebIntegrationTest */ @Documented @Inherited diff --git a/spring-boot/src/main/java/org/springframework/boot/test/IntegrationTestPropertiesListener.java b/spring-boot/src/main/java/org/springframework/boot/test/IntegrationTestPropertiesListener.java index b3cc56a283..4b732dbd1c 100644 --- a/spring-boot/src/main/java/org/springframework/boot/test/IntegrationTestPropertiesListener.java +++ b/spring-boot/src/main/java/org/springframework/boot/test/IntegrationTestPropertiesListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2104 the original author or authors. + * Copyright 2013-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. @@ -13,11 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.boot.test; -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.Set; +package org.springframework.boot.test; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationAttributes; @@ -35,14 +32,12 @@ import org.springframework.test.util.ReflectionTestUtils; */ class IntegrationTestPropertiesListener extends AbstractTestExecutionListener { - private static final String ANNOTATION_TYPE = IntegrationTest.class.getName(); - @Override public void prepareTestInstance(TestContext testContext) throws Exception { Class testClass = testContext.getTestClass(); - if (AnnotatedElementUtils.isAnnotated(testClass, ANNOTATION_TYPE)) { - AnnotationAttributes annotationAttributes = AnnotatedElementUtils - .getAnnotationAttributes(testClass, ANNOTATION_TYPE); + AnnotationAttributes annotationAttributes = AnnotatedElementUtils + .getAnnotationAttributes(testClass, IntegrationTest.class.getName()); + if (annotationAttributes != null) { addPropertySourceProperties(testContext, annotationAttributes.getStringArray("value")); } @@ -50,7 +45,9 @@ class IntegrationTestPropertiesListener extends AbstractTestExecutionListener { private void addPropertySourceProperties(TestContext testContext, String[] properties) { try { - addPropertySourcePropertiesUsingReflection(testContext, properties); + MergedContextConfiguration configuration = (MergedContextConfiguration) ReflectionTestUtils + .getField(testContext, "mergedContextConfiguration"); + new MergedContextConfigurationProperties(configuration).add(properties); } catch (RuntimeException ex) { throw ex; @@ -60,27 +57,4 @@ class IntegrationTestPropertiesListener extends AbstractTestExecutionListener { } } - private void addPropertySourcePropertiesUsingReflection(TestContext testContext, - String[] properties) throws Exception { - MergedContextConfiguration configuration = (MergedContextConfiguration) ReflectionTestUtils - .getField(testContext, "mergedContextConfiguration"); - Set merged = new LinkedHashSet((Arrays.asList(configuration - .getPropertySourceProperties()))); - merged.addAll(Arrays.asList(properties)); - addIntegrationTestProperty(merged); - ReflectionTestUtils.setField(configuration, "propertySourceProperties", - merged.toArray(new String[merged.size()])); - } - - /** - * Add an "IntegrationTest" property to ensure that there is something to - * differentiate regular tests and {@code @IntegrationTest} tests. Without this - * property a cached context could be returned that hadn't started the embedded - * servlet container. - * @param propertySourceProperties the property source properties - */ - private void addIntegrationTestProperty(Set propertySourceProperties) { - propertySourceProperties.add(IntegrationTest.class.getName() + "=true"); - } - } 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 new file mode 100644 index 0000000000..7494ab1ac4 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/test/MergedContextConfigurationProperties.java @@ -0,0 +1,60 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * Provides access to {@link MergedContextConfiguration} properties. + * + * @author Phillip Webb + * @since 1.2.1 + */ +class MergedContextConfigurationProperties { + + private final MergedContextConfiguration configuration; + + public MergedContextConfigurationProperties(MergedContextConfiguration configuration) { + this.configuration = configuration; + } + + public void add(String[] properties) { + Set merged = new LinkedHashSet((Arrays.asList(this.configuration + .getPropertySourceProperties()))); + merged.addAll(Arrays.asList(properties)); + addIntegrationTestProperty(merged); + ReflectionTestUtils.setField(this.configuration, "propertySourceProperties", + merged.toArray(new String[merged.size()])); + } + + /** + * Add an "IntegrationTest" property to ensure that there is something to + * differentiate regular tests and {@code @IntegrationTest} tests. Without this + * property a cached context could be returned that hadn't started the embedded + * servlet container. + * @param propertySourceProperties the property source properties + */ + private void addIntegrationTestProperty(Set propertySourceProperties) { + propertySourceProperties.add(IntegrationTest.class.getName() + "=true"); + } + +} 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 8bd50579af..c827e5365d 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 @@ -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. @@ -69,6 +69,7 @@ import org.springframework.web.context.support.GenericWebApplicationContext; * * @author Dave Syer * @see IntegrationTest + * @see WebIntegrationTest */ public class SpringApplicationContextLoader extends AbstractContextLoader { @@ -158,7 +159,8 @@ public class SpringApplicationContextLoader extends AbstractContextLoader { disableJmx(properties); properties.putAll(extractEnvironmentProperties(config .getPropertySourceProperties())); - if (AnnotationUtils.findAnnotation(config.getTestClass(), IntegrationTest.class) == null) { + if (!isAnnotated(config.getTestClass(), IntegrationTest.class, + WebIntegrationTest.class)) { properties.putAll(getDefaultEnvironmentProperties()); } return properties; @@ -226,8 +228,8 @@ public class SpringApplicationContextLoader extends AbstractContextLoader { SpringApplication application, List> initializers) { WebMergedContextConfiguration webConfiguration = (WebMergedContextConfiguration) configuration; - if (AnnotationUtils.findAnnotation(webConfiguration.getTestClass(), - IntegrationTest.class) == null) { + if (!isAnnotated(webConfiguration.getTestClass(), IntegrationTest.class, + WebIntegrationTest.class)) { MockServletContext servletContext = new MockServletContext( webConfiguration.getResourceBasePath()); initializers.add(0, new ServletContextApplicationContextInitializer( @@ -239,4 +241,14 @@ public class SpringApplicationContextLoader extends AbstractContextLoader { } + @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; + } + } 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 new file mode 100644 index 0000000000..4bced26e57 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/test/WebAppIntegrationTestContextBootstrapper.java @@ -0,0 +1,78 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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; + +/** + * {@link TestContextBootstrapper} for Spring Boot web integration tests. + * + * @author Phillip Webb + * @since 1.2.1 + */ +class WebAppIntegrationTestContextBootstrapper extends DefaultTestContextBootstrapper { + + @Override + protected Class getDefaultContextLoaderClass( + Class testClass) { + if (AnnotationUtils.findAnnotation(testClass, WebIntegrationTest.class) != null) { + return WebDelegatingSmartContextLoader.class; + } + return super.getDefaultContextLoaderClass(testClass); + } + + @Override + protected MergedContextConfiguration processMergedContextConfiguration( + MergedContextConfiguration mergedConfig) { + WebIntegrationTest annotation = AnnotationUtils.findAnnotation( + mergedConfig.getTestClass(), WebIntegrationTest.class); + if (annotation != null) { + mergedConfig = new WebMergedContextConfiguration(mergedConfig, null); + MergedContextConfigurationProperties properties = new MergedContextConfigurationProperties( + mergedConfig); + 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 new file mode 100644 index 0000000000..58c6e58254 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/test/WebIntegrationTest.java @@ -0,0 +1,54 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.env.Environment; +import org.springframework.test.context.BootstrapWith; + +/** + * Test class annotation signifying that the tests are "web 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}, + *

+ * This annotation can be used as an alternative to {@code @IntegrationTest} and + * {@code @WebAppConfiguration}. + * + * @author Phillip Webb + * @since 1.2.1 + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@BootstrapWith(WebAppIntegrationTestContextBootstrapper.class) +public @interface WebIntegrationTest { + + /** + * Properties in form {@literal key=value} that should be added to the Spring + * {@link Environment} before the test runs. + */ + String[] value() default {}; + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/test/SpringApplicationWebIntegrationTestTests.java b/spring-boot/src/test/java/org/springframework/boot/test/SpringApplicationWebIntegrationTestTests.java new file mode 100644 index 0000000000..b4149d762d --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/test/SpringApplicationWebIntegrationTestTests.java @@ -0,0 +1,100 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; +import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; +import org.springframework.boot.test.SpringApplicationWebIntegrationTestTests.Config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +/** + * Tests for {@link IntegrationTest} + * + * @author Phillip Webb + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = Config.class) +@WebIntegrationTest({ "server.port=0", "value=123" }) +public class SpringApplicationWebIntegrationTestTests { + + @Value("${local.server.port}") + private int port = 0; + + @Value("${value}") + private int value = 0; + + @Test + public void runAndTestHttpEndpoint() { + assertNotEquals(8080, this.port); + assertNotEquals(0, this.port); + String body = new RestTemplate().getForObject("http://localhost:" + this.port + + "/", String.class); + assertEquals("Hello World", body); + } + + @Test + public void annotationAttributesOverridePropertiesFile() throws Exception { + assertEquals(123, this.value); + } + + @Configuration + @EnableWebMvc + @RestController + protected static class Config { + + @Value("${server.port:8080}") + private int port = 8080; + + @Bean + public DispatcherServlet dispatcherServlet() { + return new DispatcherServlet(); + } + + @Bean + public EmbeddedServletContainerFactory embeddedServletContainer() { + TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory(); + factory.setPort(this.port); + return factory; + } + + @Bean + public static PropertySourcesPlaceholderConfigurer propertyPlaceholder() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @RequestMapping("/") + public String home() { + return "Hello World"; + } + + } + +}