From d251b5133882ad5cc0d787a3066b9112178204be Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Wed, 15 Oct 2014 16:59:55 +0100 Subject: [PATCH] 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 --- .../EndpointMvcIntegrationTests.java | 2 +- .../boot/test/IntegrationTest.java | 11 +- .../IntegrationTestPropertiesListener.java | 114 ++++++++++++++++++ .../test/SpringApplicationContextLoader.java | 81 +++++-------- .../springframework/boot/AdhocTestSuite.java | 3 +- ...pringApplicationConfigurationJmxTests.java | 5 +- .../SpringApplicationContextLoaderTests.java | 81 +++++++++++-- 7 files changed, 227 insertions(+), 70 deletions(-) create mode 100644 spring-boot/src/main/java/org/springframework/boot/test/IntegrationTestPropertiesListener.java diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java index a2bc94787df..804e6f714c1 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java @@ -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 { 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 7d2d2afe6f0..7c3cd2cbbea 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 @@ -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"}; } 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 new file mode 100644 index 00000000000..c5a8a67a50b --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/test/IntegrationTestPropertiesListener.java @@ -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 @IntegrationTest 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 result = new LinkedHashSet(); + 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 result = new LinkedHashSet(); + for (String value : original) { + result.add(value); + } + for (String value : extra) { + result.add(value); + } + return result.toArray(new String[0]); + } + +} 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 7e957f84507..38976740f0a 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 @@ -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> 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 extractEnvironmentProperties(String[] values) { + Map properties = new HashMap(); + 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 getEnvironmentProperties( - MergedContextConfiguration config) { - Map properties = new LinkedHashMap(); - // 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 properties) { - properties.put("spring.jmx.enabled", "false"); - } - - private Map getEnvironmentProperties(IntegrationTest annotation) { - if (annotation == null) { - return getDefaultEnvironmentProperties(); - } - return extractEnvironmentProperties(annotation.value()); - } - - private Map getDefaultEnvironmentProperties() { - return Collections.singletonMap("server.port", "-1"); - } - - // Instead of parsing the keys ourselves, we rely on standard handling - private Map 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 properties = new HashMap(); - for (String name : props.stringPropertyNames()) { - properties.put(name, props.getProperty(name)); - } - return properties; - } + + private List> getInitializers( MergedContextConfiguration mergedConfig, SpringApplication application) { diff --git a/spring-boot/src/test/java/org/springframework/boot/AdhocTestSuite.java b/spring-boot/src/test/java/org/springframework/boot/AdhocTestSuite.java index 30874b0f2bf..7499b13fba7 100644 --- a/spring-boot/src/test/java/org/springframework/boot/AdhocTestSuite.java +++ b/spring-boot/src/test/java/org/springframework/boot/AdhocTestSuite.java @@ -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 { } diff --git a/spring-boot/src/test/java/org/springframework/boot/test/SpringApplicationConfigurationJmxTests.java b/spring-boot/src/test/java/org/springframework/boot/test/SpringApplicationConfigurationJmxTests.java index 7a5d3a2c41d..8a1df6fcf3d 100644 --- a/spring-boot/src/test/java/org/springframework/boot/test/SpringApplicationConfigurationJmxTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/test/SpringApplicationConfigurationJmxTests.java @@ -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}") diff --git a/spring-boot/src/test/java/org/springframework/boot/test/SpringApplicationContextLoaderTests.java b/spring-boot/src/test/java/org/springframework/boot/test/SpringApplicationContextLoaderTests.java index d2b7c107b3f..426bda8b14b 100644 --- a/spring-boot/src/test/java/org/springframework/boot/test/SpringApplicationContextLoaderTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/test/SpringApplicationContextLoaderTests.java @@ -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 config = getEnvironmentProperties(SimpleConfig.class); assertKey(config, "key", "myValue"); assertKey(config, "anotherKey", "anotherValue"); } @Test - public void environmentPropertiesSeparatorInValue() { + public void environmentPropertiesDefaults() throws Exception { + Map config = getEnvironmentProperties(SimpleConfig.class); + assertMissingKey(config, "server.port"); + assertKey(config, "spring.jmx.enabled", "false"); + } + + @Test + public void environmentPropertiesOverrideDefaults() throws Exception { + Map 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 config = getEnvironmentProperties(AppendConfig.class); + assertKey(config, "key", "myValue"); + assertKey(config, "otherKey", "otherValue"); + } + + @Test + public void environmentPropertiesSeparatorInValue() throws Exception { Map config = getEnvironmentProperties(SameSeparatorInValue.class); assertKey(config, "key", "my=Value"); assertKey(config, "anotherKey", "another:Value"); } @Test - public void environmentPropertiesAnotherSeparatorInValue() { + public void environmentPropertiesAnotherSeparatorInValue() throws Exception { Map config = getEnvironmentProperties(AnotherSeparatorInValue.class); assertKey(config, "key", "my:Value"); assertKey(config, "anotherKey", "another=Value"); } - private Map getEnvironmentProperties(Class testClass) { - MergedContextConfiguration configuration = mock(MergedContextConfiguration.class); - doReturn(testClass).when(configuration).getTestClass(); - return this.loader.getEnvironmentProperties(configuration); + private Map 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 actual, String key, Object value) { @@ -67,10 +95,26 @@ public class SpringApplicationContextLoaderTests { assertEquals(value, actual.get(key)); } + private void assertMissingKey(Map 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(); + } + + + } }