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
This commit is contained in:
Sam Brannen 2023-08-15 18:08:44 +02:00
parent 8c2a39b5af
commit a2f52db452
5 changed files with 223 additions and 41 deletions

View File

@ -211,7 +211,8 @@ public @interface TestPropertySource {
* {@link org.springframework.core.env.Environment Environment} before the * {@link org.springframework.core.env.Environment Environment} before the
* {@code ApplicationContext} is loaded for the test. All key-value pairs * {@code ApplicationContext} is loaded for the test. All key-value pairs
* will be added to the enclosing {@code Environment} as a single test * 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 <em>text block</em>.
* <h4>Supported Syntax</h4> * <h4>Supported Syntax</h4>
* <p>The supported syntax for key-value pairs is the same as the * <p>The supported syntax for key-value pairs is the same as the
* syntax defined for entries in a Java * syntax defined for entries in a Java
@ -221,6 +222,28 @@ public @interface TestPropertySource {
* <li>{@code "key:value"}</li> * <li>{@code "key:value"}</li>
* <li>{@code "key value"}</li> * <li>{@code "key value"}</li>
* </ul> * </ul>
* <h4>Examples</h4>
* <pre class="code">
* &#47;&#47; Using an array of strings
* &#064;TestPropertySource(properties = {
* "key1 = value1",
* "key2 = value2"
* })
* &#064;ContextConfiguration
* class MyTests {
* // ...
* }</pre>
* <pre class="code">
* &#47;&#47; Using a single text block
* &#064;TestPropertySource(properties = """
* key1 = value1
* key2 = value2
* """
* )
* &#064;ContextConfiguration
* class MyTests {
* // ...
* }</pre>
* <h4>Precedence</h4> * <h4>Precedence</h4>
* <p>Properties declared via this attribute have higher precedence than * <p>Properties declared via this attribute have higher precedence than
* properties loaded from resource {@link #locations}. * properties loaded from resource {@link #locations}.

View File

@ -367,18 +367,22 @@ public abstract class TestPropertySourceUtils {
/** /**
* Convert the supplied <em>inlined properties</em> (in the form of <em>key-value</em> * Convert the supplied <em>inlined properties</em> (in the form of <em>key-value</em>
* pairs) into a map keyed by property name, preserving the ordering of property names * pairs) into a map keyed by property name.
* in the returned map. * <p>Parsing of the key-value pairs is achieved by converting all supplied
* <p>Parsing of the key-value pairs is achieved by converting all pairs * strings into <em>virtual</em> properties files in memory and delegating to
* into <em>virtual</em> properties files in memory and delegating to
* {@link Properties#load(java.io.Reader)} to parse each virtual file. * {@link Properties#load(java.io.Reader)} to parse each virtual file.
* <p>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 &mdash; for example, when supplied by
* a user via a <em>text block</em> &mdash; the ordering of property names for
* those particular key-value pairs cannot be guaranteed in the returned map.
* <p>For a full discussion of <em>inlined properties</em>, consult the Javadoc * <p>For a full discussion of <em>inlined properties</em>, consult the Javadoc
* for {@link TestPropertySource#properties}. * for {@link TestPropertySource#properties}.
* @param inlinedProperties the inlined properties to convert; potentially empty * @param inlinedProperties the inlined properties to convert; potentially empty
* but never {@code null} * but never {@code null}
* @return a new, ordered map containing the converted properties * @return a new, ordered map containing the converted properties
* @throws IllegalStateException if a given key-value pair cannot be parsed, or if * @throws IllegalStateException if a given key-value pair cannot be parsed
* a given inlined property contains multiple key-value pairs
* @since 4.1.5 * @since 4.1.5
* @see #addInlinedPropertiesToEnvironment(ConfigurableEnvironment, String[]) * @see #addInlinedPropertiesToEnvironment(ConfigurableEnvironment, String[])
*/ */
@ -395,9 +399,8 @@ public abstract class TestPropertySourceUtils {
props.load(new StringReader(pair)); props.load(new StringReader(pair));
} }
catch (Exception ex) { 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()) { for (String name : props.stringPropertyNames()) {
map.put(name, props.getProperty(name)); map.put(name, props.getProperty(name));
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 * @author Sam Brannen
* @since 4.1 * @since 4.1
* @see InlinedPropertiesWithTextBlockTestPropertySourceTests
*/ */
@ExtendWith(SpringExtension.class) @ExtendWith(SpringExtension.class)
@ContextConfiguration @ContextConfiguration
@ -44,40 +45,37 @@ import static org.springframework.test.context.support.TestPropertySourceUtils.I
class InlinedPropertiesTestPropertySourceTests { class InlinedPropertiesTestPropertySourceTests {
@Autowired @Autowired
private ConfigurableEnvironment env; ConfigurableEnvironment env;
private String property(String key) {
return env.getProperty(key);
}
@Test @Test
void propertiesAreAvailableInEnvironment() { void propertiesAreAvailableInEnvironment() {
// Simple key/value pairs // Simple key/value pairs
assertThat(property("foo")).isEqualTo("bar"); assertEnvironmentProperty("foo", "bar");
assertThat(property("baz")).isEqualTo("quux"); assertEnvironmentProperty("baz", "quux");
assertThat(property("enigma")).isEqualTo("42"); assertEnvironmentProperty("enigma", "42");
// Values containing key/value delimiters (":", "=", " ") // Values containing key/value delimiters (":", "=", " ")
assertThat(property("x.y.z")).isEqualTo("a=b=c"); assertEnvironmentProperty("x.y.z", "a=b=c");
assertThat(property("server.url")).isEqualTo("https://example.com"); assertEnvironmentProperty("server.url", "https://example.com");
assertThat(property("key.value.1")).isEqualTo("key=value"); assertEnvironmentProperty("key.value.1", "key=value");
assertThat(property("key.value.2")).isEqualTo("key=value"); assertEnvironmentProperty("key.value.2", "key=value");
assertThat(property("key.value.3")).isEqualTo("key:value"); assertEnvironmentProperty("key.value.3", "key:value");
} }
@Test @Test
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
void propertyNameOrderingIsPreservedInEnvironment() { 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( EnumerablePropertySource eps = (EnumerablePropertySource) env.getPropertySources().get(
INLINED_PROPERTIES_PROPERTY_SOURCE_NAME); 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 @Configuration
static class Config { static class Config {

View File

@ -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 */
}
}

View File

@ -19,7 +19,6 @@ package org.springframework.test.context.support;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.assertj.core.api.SoftAssertions; import org.assertj.core.api.SoftAssertions;
@ -28,7 +27,9 @@ import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AnnotationConfigurationException; import org.springframework.core.annotation.AnnotationConfigurationException;
import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PropertySourceDescriptor; 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.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; 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.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock; 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.addInlinedPropertiesToEnvironment;
import static org.springframework.test.context.support.TestPropertySourceUtils.addPropertiesFilesToEnvironment; import static org.springframework.test.context.support.TestPropertySourceUtils.addPropertiesFilesToEnvironment;
import static org.springframework.test.context.support.TestPropertySourceUtils.buildMergedTestPropertySources; 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[] 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 @Test
void emptyAnnotation() { void emptyAnnotation() {
assertThatIllegalStateException() assertThatIllegalStateException()
.isThrownBy(() -> buildMergedTestPropertySources(EmptyPropertySources.class)) .isThrownBy(() -> buildMergedTestPropertySources(EmptyPropertySources.class))
.withMessageStartingWith("Could not detect default properties file for test class") .withMessageContainingAll(
.withMessageContaining("class path resource") "Could not detect default properties file for test class",
.withMessageContaining("does not exist") "class path resource",
.withMessageContaining("EmptyPropertySources.properties"); "does not exist",
"EmptyPropertySources.properties");
} }
@Test @Test
@ -260,16 +264,26 @@ class TestPropertySourceUtilsTests {
@Test @Test
void addInlinedPropertiesToEnvironmentWithMalformedUnicodeInValue() { void addInlinedPropertiesToEnvironmentWithMalformedUnicodeInValue() {
String properties = "key = \\uZZZZ";
assertThatIllegalStateException() assertThatIllegalStateException()
.isThrownBy(() -> addInlinedPropertiesToEnvironment(new MockEnvironment(), asArray("key = \\uZZZZ"))) .isThrownBy(() -> addInlinedPropertiesToEnvironment(new MockEnvironment(), properties))
.withMessageContaining("Failed to load test environment property"); .withMessageContaining("Failed to load test environment properties from [%s]", properties);
} }
@Test @Test
void addInlinedPropertiesToEnvironmentWithMultipleKeyValuePairsInSingleInlinedProperty() { void addInlinedPropertiesToEnvironmentWithMultipleKeyValuePairsInSingleInlinedProperty() {
assertThatIllegalStateException() ConfigurableEnvironment environment = new MockEnvironment();
.isThrownBy(() -> addInlinedPropertiesToEnvironment(new MockEnvironment(), asArray("a=b\nx=y"))) MutablePropertySources propertySources = environment.getPropertySources();
.withMessageContaining("Failed to load exactly one test environment property"); 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 @Test
@ -279,9 +293,11 @@ class TestPropertySourceUtilsTests {
MutablePropertySources propertySources = environment.getPropertySources(); MutablePropertySources propertySources = environment.getPropertySources();
propertySources.remove(MockPropertySource.MOCK_PROPERTIES_PROPERTY_SOURCE_NAME); propertySources.remove(MockPropertySource.MOCK_PROPERTIES_PROPERTY_SOURCE_NAME);
assertThat(propertySources).isEmpty(); assertThat(propertySources).isEmpty();
addInlinedPropertiesToEnvironment(environment, asArray(" ")); addInlinedPropertiesToEnvironment(environment, " ");
assertThat(propertySources).hasSize(1); 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 @Test