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
This commit is contained in:
Phillip Webb
2015-01-06 19:03:29 -08:00
parent 165b85dd0e
commit be30385e15
9 changed files with 332 additions and 53 deletions

View File

@@ -365,15 +365,14 @@ that and be sure that it has initialized is to add a `@Bean` of type
`ApplicationListener<EmbeddedServletContainerInitializedEvent>` 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

View File

@@ -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 <<boot-features-testing-spring-boot-applications,described above>>
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.

View File

@@ -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}.
* <p>
* If your test also uses {@code @WebAppConfiguration} consider using the
* {@link WebIntegrationTest} instead.
*
* @author Dave Syer
* @see WebIntegrationTest
*/
@Documented
@Inherited

View File

@@ -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<String> merged = new LinkedHashSet<String>((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<String> propertySourceProperties) {
propertySourceProperties.add(IntegrationTest.class.getName() + "=true");
}
}

View File

@@ -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<String> merged = new LinkedHashSet<String>((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<String> propertySourceProperties) {
propertySourceProperties.add(IntegrationTest.class.getName() + "=true");
}
}

View File

@@ -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<ApplicationContextInitializer<?>> 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;
}
}

View File

@@ -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<? extends ContextLoader> 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<String> getDefaultTestExecutionListenerClassNames() {
WebIntegrationTest annotation = AnnotationUtils.findAnnotation(
getBootstrapContext().getTestClass(), WebIntegrationTest.class);
List<String> 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<String>(listeners);
listeners.remove(ServletTestExecutionListener.class.getName());
listeners.add(IntegrationTestPropertiesListener.class.getName());
}
return Collections.unmodifiableList(listeners);
}
}

View File

@@ -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},
* <p>
* 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 {};
}

View File

@@ -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";
}
}
}