Integrate with @TestPropertySource

Spring 4.2 has a @TestPropertySource which has some of the features of
@IntegrationTest. This change adds @TestPropertySource to the @IntegrationTest
annotation, so that (for instance) the cache key for the context includes
properties for the test.

Since @IntegrationTest has slightly different semantics I do not propose to
deprecate it. Users can use it or @TestPropertySource, the main difference being that
with @IntegrationTest the Spring Boot context loader is aware of the annotation
and it will set sensible defaults for server.port and spring.jmx.enabled.

There are some reflection hacks to overcome the usual fortifications of Spring Test.

Fixes gh-1697
This commit is contained in:
Dave Syer 2014-10-15 16:59:55 +01:00
parent 261b3afca1
commit d251b51338
7 changed files with 227 additions and 70 deletions

View File

@ -58,8 +58,8 @@ import static org.junit.Assert.assertTrue;
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@IntegrationTest("server.port=0")
@WebAppConfiguration
@DirtiesContext
public class EndpointMvcIntegrationTests {

View File

@ -25,6 +25,7 @@ import java.lang.annotation.Target;
import org.springframework.core.env.Environment;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
@ -41,15 +42,21 @@ import org.springframework.test.context.transaction.TransactionalTestExecutionLi
@Target(ElementType.TYPE)
// Leave out the ServletTestExecutionListener because it only deals with Mock* servlet
// stuff. A real embedded application will not need the mocks.
@TestExecutionListeners(listeners = { DependencyInjectionTestExecutionListener.class,
@TestExecutionListeners(listeners = { IntegrationTestPropertiesListener.class, DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class })
@TestPropertySource
public @interface IntegrationTest {
/**
* Synonym for properties().
*/
String[] value() default {};
/**
* Properties in form {@literal key=value} that should be added to the Spring
* {@link Environment} before the test runs.
*/
String[] value() default {};
String[] properties() default {"server.port=-1", "spring.jmx.enabled=false"};
}

View File

@ -0,0 +1,114 @@
/*
* Copyright 2013-2104 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.core.annotation.AnnotationUtils;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.test.util.ReflectionTestUtils;
/**
* Manipulate the TestContext to merge properties from <code>@IntegrationTest</code> value
* and properties attributes.
*
* @author Dave Syer
*
*/
public class IntegrationTestPropertiesListener extends AbstractTestExecutionListener {
private String[] defaultValues = (String[]) AnnotationUtils.getDefaultValue(
IntegrationTest.class, "properties");
@Override
public void prepareTestInstance(TestContext testContext) throws Exception {
MergedContextConfiguration config = null;
try {
// Here be hacks...
config = (MergedContextConfiguration) ReflectionTestUtils.getField(
testContext, "mergedContextConfiguration");
ReflectionTestUtils.setField(config, "propertySourceProperties",
getEnvironmentProperties(config));
}
catch (IllegalStateException e) {
throw e;
}
catch (Exception e) {
}
}
protected String[] getEnvironmentProperties(MergedContextConfiguration config) {
IntegrationTest annotation = AnnotationUtils.findAnnotation(
config.getTestClass(), IntegrationTest.class);
return mergeProperties(
getDefaultEnvironmentProperties(config.getPropertySourceProperties(),
annotation), getEnvironmentProperties(annotation));
}
private String[] getDefaultEnvironmentProperties(String[] original,
IntegrationTest annotation) {
String[] defaults = mergeProperties(original, defaultValues);
if (annotation == null || defaults.length == 0) {
// Without an @IntegrationTest we can assume the defaults are fine
return defaults;
}
// If @IntegrationTest is present we don't provide a default for the server.port
return filterPorts((String[]) AnnotationUtils.getDefaultValue(annotation,
"properties"));
}
private String[] filterPorts(String[] values) {
Set<String> result = new LinkedHashSet<String>();
for (String value : values) {
if (!value.contains(".port")) {
result.add(value);
}
}
return result.toArray(new String[0]);
}
private String[] getEnvironmentProperties(IntegrationTest annotation) {
if (annotation == null) {
return new String[0];
}
if (Arrays.asList(annotation.properties()).equals(Arrays.asList(defaultValues))) {
return annotation.value();
}
if (annotation.value().length == 0) {
return annotation.properties();
}
throw new IllegalStateException(
"Either properties or value can be provided but not both");
}
private String[] mergeProperties(String[] original, String[] extra) {
Set<String> result = new LinkedHashSet<String>();
for (String value : original) {
result.add(value);
}
for (String value : extra) {
result.add(value);
}
return result.toArray(new String[0]);
}
}

View File

@ -20,9 +20,7 @@ import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@ -92,7 +90,7 @@ public class SpringApplicationContextLoader extends AbstractContextLoader {
.addAfter(
StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
new MapPropertySource("integrationTest",
getEnvironmentProperties(config)));
extractEnvironmentProperties(config.getPropertySourceProperties())));
application.setEnvironment(environment);
List<ApplicationContextInitializer<?>> initializers = getInitializers(config,
application);
@ -107,6 +105,32 @@ public class SpringApplicationContextLoader extends AbstractContextLoader {
return application.run();
}
// Instead of parsing the keys ourselves, we rely on standard handling
protected Map<String, Object> extractEnvironmentProperties(String[] values) {
Map<String, Object> properties = new HashMap<String, Object>();
if (values==null) {
return properties;
}
StringBuilder sb = new StringBuilder();
for (String value : values) {
sb.append(value).append(LINE_SEPARATOR);
}
String content = sb.toString();
Properties props = new Properties();
try {
props.load(new StringReader(content));
}
catch (IOException e) {
throw new IllegalStateException("Unexpected could not load properties from '"
+ content + "'", e);
}
for (String name : props.stringPropertyNames()) {
properties.put(name, props.getProperty(name));
}
return properties;
}
@Override
public void processContextConfiguration(
ContextConfigurationAttributes configAttributes) {
@ -152,55 +176,8 @@ public class SpringApplicationContextLoader extends AbstractContextLoader {
return AnnotationConfigContextLoaderUtils
.detectDefaultConfigurationClasses(declaringClass);
}
protected Map<String, Object> getEnvironmentProperties(
MergedContextConfiguration config) {
Map<String, Object> properties = new LinkedHashMap<String, Object>();
// JMX bean names will clash if the same bean is used in multiple contexts
disableJmx(properties);
IntegrationTest annotation = AnnotationUtils.findAnnotation(
config.getTestClass(), IntegrationTest.class);
properties.putAll(getEnvironmentProperties(annotation));
return properties;
}
private void disableJmx(Map<String, Object> properties) {
properties.put("spring.jmx.enabled", "false");
}
private Map<String, String> getEnvironmentProperties(IntegrationTest annotation) {
if (annotation == null) {
return getDefaultEnvironmentProperties();
}
return extractEnvironmentProperties(annotation.value());
}
private Map<String, String> getDefaultEnvironmentProperties() {
return Collections.singletonMap("server.port", "-1");
}
// Instead of parsing the keys ourselves, we rely on standard handling
private Map<String, String> extractEnvironmentProperties(String[] values) {
StringBuilder sb = new StringBuilder();
for (String value : values) {
sb.append(value).append(LINE_SEPARATOR);
}
String content = sb.toString();
Properties props = new Properties();
try {
props.load(new StringReader(content));
}
catch (IOException e) {
throw new IllegalStateException("Unexpected could not load properties from '"
+ content + "'", e);
}
Map<String, String> properties = new HashMap<String, String>();
for (String name : props.stringPropertyNames()) {
properties.put(name, props.getProperty(name));
}
return properties;
}
private List<ApplicationContextInitializer<?>> getInitializers(
MergedContextConfiguration mergedConfig, SpringApplication application) {

View File

@ -16,6 +16,7 @@
package org.springframework.boot;
import org.junit.Ignore;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
@ -30,7 +31,7 @@ import org.springframework.boot.test.SpringApplicationConfigurationJmxTests;
@RunWith(Suite.class)
@SuiteClasses({ SpringApplicationConfigurationJmxTests.class,
SpringApplicationConfigurationDefaultConfigurationTests.class })
// @Ignore
@Ignore
public class AdhocTestSuite {
}

View File

@ -16,6 +16,8 @@
package org.springframework.boot.test;
import static org.junit.Assert.assertFalse;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
@ -25,8 +27,6 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.junit.Assert.assertFalse;
/**
* Tests for disabling JMX by default
*
@ -34,6 +34,7 @@ import static org.junit.Assert.assertFalse;
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Config.class)
@IntegrationTest
public class SpringApplicationConfigurationJmxTests {
@Value("${spring.jmx.enabled}")

View File

@ -16,15 +16,16 @@
package org.springframework.boot.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.Map;
import org.junit.Test;
import org.springframework.test.context.MergedContextConfiguration;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestContextManager;
import org.springframework.test.util.ReflectionTestUtils;
/**
* Tests for {@link SpringApplicationContextLoader}
@ -36,30 +37,57 @@ public class SpringApplicationContextLoaderTests {
private final SpringApplicationContextLoader loader = new SpringApplicationContextLoader();
@Test
public void environmentPropertiesSimple() {
public void environmentPropertiesSimple() throws Exception {
Map<String, Object> config = getEnvironmentProperties(SimpleConfig.class);
assertKey(config, "key", "myValue");
assertKey(config, "anotherKey", "anotherValue");
}
@Test
public void environmentPropertiesSeparatorInValue() {
public void environmentPropertiesDefaults() throws Exception {
Map<String, Object> config = getEnvironmentProperties(SimpleConfig.class);
assertMissingKey(config, "server.port");
assertKey(config, "spring.jmx.enabled", "false");
}
@Test
public void environmentPropertiesOverrideDefaults() throws Exception {
Map<String, Object> config = getEnvironmentProperties(OverrideConfig.class);
assertKey(config, "server.port", "2345");
}
@Test(expected=IllegalStateException.class)
public void environmentPropertiesIllegal() throws Exception {
getEnvironmentProperties(IllegalConfig.class);
}
@Test
public void environmentPropertiesAppend() throws Exception {
Map<String, Object> config = getEnvironmentProperties(AppendConfig.class);
assertKey(config, "key", "myValue");
assertKey(config, "otherKey", "otherValue");
}
@Test
public void environmentPropertiesSeparatorInValue() throws Exception {
Map<String, Object> config = getEnvironmentProperties(SameSeparatorInValue.class);
assertKey(config, "key", "my=Value");
assertKey(config, "anotherKey", "another:Value");
}
@Test
public void environmentPropertiesAnotherSeparatorInValue() {
public void environmentPropertiesAnotherSeparatorInValue() throws Exception {
Map<String, Object> config = getEnvironmentProperties(AnotherSeparatorInValue.class);
assertKey(config, "key", "my:Value");
assertKey(config, "anotherKey", "another=Value");
}
private Map<String, Object> getEnvironmentProperties(Class<?> testClass) {
MergedContextConfiguration configuration = mock(MergedContextConfiguration.class);
doReturn(testClass).when(configuration).getTestClass();
return this.loader.getEnvironmentProperties(configuration);
private Map<String, Object> getEnvironmentProperties(Class<?> testClass) throws Exception {
TestContext context = new ExposedTestContextManager(testClass).getExposedTestContext();
new IntegrationTestPropertiesListener().prepareTestInstance(context);
MergedContextConfiguration config = (MergedContextConfiguration) ReflectionTestUtils.getField(
context, "mergedContextConfiguration");
return this.loader.extractEnvironmentProperties(config.getPropertySourceProperties());
}
private void assertKey(Map<String, Object> actual, String key, Object value) {
@ -67,10 +95,26 @@ public class SpringApplicationContextLoaderTests {
assertEquals(value, actual.get(key));
}
private void assertMissingKey(Map<String, Object> actual, String key) {
assertTrue("Key '" + key + "' found", !actual.containsKey(key));
}
@IntegrationTest({ "key=myValue", "anotherKey:anotherValue" })
static class SimpleConfig {
}
@IntegrationTest({ "server.port=2345" })
static class OverrideConfig {
}
@IntegrationTest(value = { "key=aValue", "anotherKey:anotherValue" }, properties = { "key=myValue", "otherKey=otherValue" })
static class IllegalConfig {
}
@IntegrationTest(properties = { "key=myValue", "otherKey=otherValue" })
static class AppendConfig {
}
@IntegrationTest({ "key=my=Value", "anotherKey:another:Value" })
static class SameSeparatorInValue {
}
@ -78,5 +122,18 @@ public class SpringApplicationContextLoaderTests {
@IntegrationTest({ "key=my:Value", "anotherKey:another=Value" })
static class AnotherSeparatorInValue {
}
private static class ExposedTestContextManager extends TestContextManager {
public ExposedTestContextManager(Class<?> testClass) {
super(testClass);
}
public final TestContext getExposedTestContext() {
return super.getTestContext();
}
}
}