From a2f52db452721dd08bf06d656929921326e9bce7 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 15 Aug 2023 18:08:44 +0200 Subject: [PATCH] Support text blocks for inlined properties in @TestPropertySource Prior to this commit, inlined properties could only be supplied as an array of Strings as follows. @TestPropertySource(properties = { "key1 = value1", "key2 = value2" }) Although a user could supply a text block, it was previously rejected due to a "single key-value pair per string" check in TestPropertySourceUtils.convertInlinedPropertiesToMap(String...). This commit removes that restriction and allows the above example to be refactored to use a text block as follows. @TestPropertySource(properties = """ key1 = value1 key2 = value2 """ ) Closes gh-31053 --- .../test/context/TestPropertySource.java | 25 ++- .../support/TestPropertySourceUtils.java | 19 ++- ...inedPropertiesTestPropertySourceTests.java | 34 ++--- ...sWithTextBlockTestPropertySourceTests.java | 142 ++++++++++++++++++ .../support/TestPropertySourceUtilsTests.java | 44 ++++-- 5 files changed, 223 insertions(+), 41 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/env/InlinedPropertiesWithTextBlockTestPropertySourceTests.java diff --git a/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java b/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java index e224bdbb28..292ff74fe0 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java @@ -211,7 +211,8 @@ public @interface TestPropertySource { * {@link org.springframework.core.env.Environment Environment} before the * {@code ApplicationContext} is loaded for the test. All key-value pairs * will be added to the enclosing {@code Environment} as a single test - * {@code PropertySource} with the highest precedence. + * {@code PropertySource} with the highest precedence. As of Spring Framework + * 6.1, multiple key-value pairs may be specified via a single text block. *

Supported Syntax

*

The supported syntax for key-value pairs is the same as the * syntax defined for entries in a Java @@ -221,6 +222,28 @@ public @interface TestPropertySource { *

  • {@code "key:value"}
  • *
  • {@code "key value"}
  • * + *

    Examples

    + *
    +	 * // Using an array of strings
    +	 * @TestPropertySource(properties = {
    +	 *     "key1 = value1",
    +	 *     "key2 = value2"
    +	 * })
    +	 * @ContextConfiguration
    +	 * class MyTests {
    +	 *   // ...
    +	 * }
    + *
    +	 * // Using a single text block
    +	 * @TestPropertySource(properties = """
    +	 *     key1 = value1
    +	 *     key2 = value2
    +	 *     """
    +	 * )
    +	 * @ContextConfiguration
    +	 * class MyTests {
    +	 *   // ...
    +	 * }
    *

    Precedence

    *

    Properties declared via this attribute have higher precedence than * properties loaded from resource {@link #locations}. diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java index 9050c22ee1..5a2514f609 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java @@ -367,18 +367,22 @@ public abstract class TestPropertySourceUtils { /** * Convert the supplied inlined properties (in the form of key-value - * pairs) into a map keyed by property name, preserving the ordering of property names - * in the returned map. - *

    Parsing of the key-value pairs is achieved by converting all pairs - * into virtual properties files in memory and delegating to + * pairs) into a map keyed by property name. + *

    Parsing of the key-value pairs is achieved by converting all supplied + * strings into virtual properties files in memory and delegating to * {@link Properties#load(java.io.Reader)} to parse each virtual file. + *

    Generally speaking, the ordering of property names will be preserved in + * the returned map, analogous to the order in which the key-value pairs are + * supplied to this method. However, if a single string contains multiple + * key-value pairs separated by newlines — for example, when supplied by + * a user via a text block — the ordering of property names for + * those particular key-value pairs cannot be guaranteed in the returned map. *

    For a full discussion of inlined properties, consult the Javadoc * for {@link TestPropertySource#properties}. * @param inlinedProperties the inlined properties to convert; potentially empty * but never {@code null} * @return a new, ordered map containing the converted properties - * @throws IllegalStateException if a given key-value pair cannot be parsed, or if - * a given inlined property contains multiple key-value pairs + * @throws IllegalStateException if a given key-value pair cannot be parsed * @since 4.1.5 * @see #addInlinedPropertiesToEnvironment(ConfigurableEnvironment, String[]) */ @@ -395,9 +399,8 @@ public abstract class TestPropertySourceUtils { props.load(new StringReader(pair)); } catch (Exception ex) { - throw new IllegalStateException("Failed to load test environment property from [" + pair + "]", ex); + throw new IllegalStateException("Failed to load test environment properties from [" + pair + "]", ex); } - Assert.state(props.size() == 1, () -> "Failed to load exactly one test environment property from [" + pair + "]"); for (String name : props.stringPropertyNames()) { map.put(name, props.getProperty(name)); } diff --git a/spring-test/src/test/java/org/springframework/test/context/env/InlinedPropertiesTestPropertySourceTests.java b/spring-test/src/test/java/org/springframework/test/context/env/InlinedPropertiesTestPropertySourceTests.java index efc89c4758..f0ce45a6a5 100644 --- a/spring-test/src/test/java/org/springframework/test/context/env/InlinedPropertiesTestPropertySourceTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/env/InlinedPropertiesTestPropertySourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -36,6 +36,7 @@ import static org.springframework.test.context.support.TestPropertySourceUtils.I * * @author Sam Brannen * @since 4.1 + * @see InlinedPropertiesWithTextBlockTestPropertySourceTests */ @ExtendWith(SpringExtension.class) @ContextConfiguration @@ -44,40 +45,37 @@ import static org.springframework.test.context.support.TestPropertySourceUtils.I class InlinedPropertiesTestPropertySourceTests { @Autowired - private ConfigurableEnvironment env; + ConfigurableEnvironment env; - private String property(String key) { - return env.getProperty(key); - } - @Test void propertiesAreAvailableInEnvironment() { // Simple key/value pairs - assertThat(property("foo")).isEqualTo("bar"); - assertThat(property("baz")).isEqualTo("quux"); - assertThat(property("enigma")).isEqualTo("42"); + assertEnvironmentProperty("foo", "bar"); + assertEnvironmentProperty("baz", "quux"); + assertEnvironmentProperty("enigma", "42"); // Values containing key/value delimiters (":", "=", " ") - assertThat(property("x.y.z")).isEqualTo("a=b=c"); - assertThat(property("server.url")).isEqualTo("https://example.com"); - assertThat(property("key.value.1")).isEqualTo("key=value"); - assertThat(property("key.value.2")).isEqualTo("key=value"); - assertThat(property("key.value.3")).isEqualTo("key:value"); + assertEnvironmentProperty("x.y.z", "a=b=c"); + assertEnvironmentProperty("server.url", "https://example.com"); + assertEnvironmentProperty("key.value.1", "key=value"); + assertEnvironmentProperty("key.value.2", "key=value"); + assertEnvironmentProperty("key.value.3", "key:value"); } @Test @SuppressWarnings("rawtypes") void propertyNameOrderingIsPreservedInEnvironment() { - final String[] expectedPropertyNames = new String[] { "foo", "baz", "enigma", "x.y.z", "server.url", - "key.value.1", "key.value.2", "key.value.3" }; EnumerablePropertySource eps = (EnumerablePropertySource) env.getPropertySources().get( INLINED_PROPERTIES_PROPERTY_SOURCE_NAME); - assertThat(eps.getPropertyNames()).isEqualTo(expectedPropertyNames); + assertThat(eps.getPropertyNames()).containsExactly("foo", "baz", "enigma", "x.y.z", "server.url", + "key.value.1", "key.value.2", "key.value.3" ); } + private void assertEnvironmentProperty(String name, Object value) { + assertThat(this.env.getProperty(name)).as("environment property '%s'", name).isEqualTo(value); + } - // ------------------------------------------------------------------- @Configuration static class Config { diff --git a/spring-test/src/test/java/org/springframework/test/context/env/InlinedPropertiesWithTextBlockTestPropertySourceTests.java b/spring-test/src/test/java/org/springframework/test/context/env/InlinedPropertiesWithTextBlockTestPropertySourceTests.java new file mode 100644 index 0000000000..95bc931bcd --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/env/InlinedPropertiesWithTextBlockTestPropertySourceTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2023 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 + * + * https://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.test.context.env; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.Environment; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.context.support.TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME; + +/** + * Integration tests for {@link TestPropertySource @TestPropertySource} support + * with inlined properties supplied via text blocks. + * + * @author Sam Brannen + * @since 6.1 + * @see InlinedPropertiesTestPropertySourceTests + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration +@DirtiesContext +class InlinedPropertiesWithTextBlockTestPropertySourceTests { + + @Nested + @DirtiesContext + @TestPropertySource(properties = """ + foo = bar + baz quux + enigma: 42 + x.y.z = a=b=c + server.url = https://example.com + key.value.1: key=value + key.value.2 key=value + key.value.3 key:value + """) + class AllInOneTextBlockTests { + + @Autowired + ConfigurableEnvironment env; + + @Test + void propertiesAreAvailableInEnvironment() { + // Simple key/value pairs + assertEnvironmentProperty(this.env, "foo", "bar"); + assertEnvironmentProperty(this.env, "baz", "quux"); + assertEnvironmentProperty(this.env, "enigma", "42"); + + // Values containing key/value delimiters (":", "=", " ") + assertEnvironmentProperty(this.env, "x.y.z", "a=b=c"); + assertEnvironmentProperty(this.env, "server.url", "https://example.com"); + assertEnvironmentProperty(this.env, "key.value.1", "key=value"); + assertEnvironmentProperty(this.env, "key.value.2", "key=value"); + assertEnvironmentProperty(this.env, "key.value.3", "key:value"); + } + + /** + * Not necessarily preserved because the properties are all added at the + * same time. + */ + @Test + @SuppressWarnings("rawtypes") + void propertyNameOrderingIsNotNecessarilyPreservedInEnvironment() { + EnumerablePropertySource eps = (EnumerablePropertySource) env.getPropertySources().get( + INLINED_PROPERTIES_PROPERTY_SOURCE_NAME); + assertThat(eps.getPropertyNames()).containsExactlyInAnyOrder("foo", "baz", "enigma", "x.y.z", + "server.url", "key.value.1", "key.value.2", "key.value.3"); + } + + } + + @Nested + @DirtiesContext + @TestPropertySource(properties = { + """ + foo = bar + """, + """ + bar = baz + """, + """ + baz = quux + """ + }) + class MultipleTextBlockTests { + + @Autowired + ConfigurableEnvironment env; + + @Test + void propertiesAreAvailableInEnvironment() { + assertEnvironmentProperty(this.env, "foo", "bar"); + assertEnvironmentProperty(this.env, "bar", "baz"); + assertEnvironmentProperty(this.env, "baz", "quux"); + } + + @Test + @SuppressWarnings("rawtypes") + void propertyNameOrderingIsPreservedInEnvironment() { + EnumerablePropertySource eps = (EnumerablePropertySource) env.getPropertySources().get( + INLINED_PROPERTIES_PROPERTY_SOURCE_NAME); + assertThat(eps.getPropertyNames()).containsExactly("foo", "bar", "baz"); + } + + } + + + static void assertEnvironmentProperty(Environment env, String name, Object value) { + assertThat(env.getProperty(name)).as("environment property '%s'", name).isEqualTo(value); + } + + + @Configuration + static class Config { + /* no user beans required for these tests */ + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java index 7eeb91b753..724e240fb7 100644 --- a/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java @@ -19,7 +19,6 @@ package org.springframework.test.context.support; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; -import java.util.Map; import java.util.stream.Stream; import org.assertj.core.api.SoftAssertions; @@ -28,7 +27,9 @@ import org.junit.jupiter.api.Test; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.PropertySourceDescriptor; @@ -40,9 +41,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.entry; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.springframework.test.context.support.TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME; import static org.springframework.test.context.support.TestPropertySourceUtils.addInlinedPropertiesToEnvironment; import static org.springframework.test.context.support.TestPropertySourceUtils.addPropertiesFilesToEnvironment; import static org.springframework.test.context.support.TestPropertySourceUtils.buildMergedTestPropertySources; @@ -58,19 +61,20 @@ class TestPropertySourceUtilsTests { private static final String[] EMPTY_STRING_ARRAY = new String[0]; - private static final String[] KEY_VALUE_PAIR = new String[] {"key = value"}; + private static final String[] KEY_VALUE_PAIR = {"key = value"}; - private static final String[] FOO_LOCATIONS = new String[] {"classpath:/foo.properties"}; + private static final String[] FOO_LOCATIONS = {"classpath:/foo.properties"}; @Test void emptyAnnotation() { assertThatIllegalStateException() .isThrownBy(() -> buildMergedTestPropertySources(EmptyPropertySources.class)) - .withMessageStartingWith("Could not detect default properties file for test class") - .withMessageContaining("class path resource") - .withMessageContaining("does not exist") - .withMessageContaining("EmptyPropertySources.properties"); + .withMessageContainingAll( + "Could not detect default properties file for test class", + "class path resource", + "does not exist", + "EmptyPropertySources.properties"); } @Test @@ -260,16 +264,26 @@ class TestPropertySourceUtilsTests { @Test void addInlinedPropertiesToEnvironmentWithMalformedUnicodeInValue() { + String properties = "key = \\uZZZZ"; assertThatIllegalStateException() - .isThrownBy(() -> addInlinedPropertiesToEnvironment(new MockEnvironment(), asArray("key = \\uZZZZ"))) - .withMessageContaining("Failed to load test environment property"); + .isThrownBy(() -> addInlinedPropertiesToEnvironment(new MockEnvironment(), properties)) + .withMessageContaining("Failed to load test environment properties from [%s]", properties); } @Test void addInlinedPropertiesToEnvironmentWithMultipleKeyValuePairsInSingleInlinedProperty() { - assertThatIllegalStateException() - .isThrownBy(() -> addInlinedPropertiesToEnvironment(new MockEnvironment(), asArray("a=b\nx=y"))) - .withMessageContaining("Failed to load exactly one test environment property"); + ConfigurableEnvironment environment = new MockEnvironment(); + MutablePropertySources propertySources = environment.getPropertySources(); + propertySources.remove(MockPropertySource.MOCK_PROPERTIES_PROPERTY_SOURCE_NAME); + assertThat(propertySources).isEmpty(); + addInlinedPropertiesToEnvironment(environment, """ + a=b + x=y + """); + assertThat(propertySources).hasSize(1); + PropertySource propertySource = propertySources.get(INLINED_PROPERTIES_PROPERTY_SOURCE_NAME); + assertThat(propertySource).isInstanceOf(MapPropertySource.class); + assertThat(((MapPropertySource) propertySource).getSource()).containsExactly(entry("a", "b"), entry("x", "y")); } @Test @@ -279,9 +293,11 @@ class TestPropertySourceUtilsTests { MutablePropertySources propertySources = environment.getPropertySources(); propertySources.remove(MockPropertySource.MOCK_PROPERTIES_PROPERTY_SOURCE_NAME); assertThat(propertySources).isEmpty(); - addInlinedPropertiesToEnvironment(environment, asArray(" ")); + addInlinedPropertiesToEnvironment(environment, " "); assertThat(propertySources).hasSize(1); - assertThat(((Map) propertySources.iterator().next().getSource())).isEmpty(); + PropertySource propertySource = propertySources.get(INLINED_PROPERTIES_PROPERTY_SOURCE_NAME); + assertThat(propertySource).isInstanceOf(MapPropertySource.class); + assertThat(((MapPropertySource) propertySource).getSource()).isEmpty(); } @Test