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(); + } + + + } }