Skip to content

Commit a2f52db

Browse files
committed
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
1 parent 8c2a39b commit a2f52db

File tree

5 files changed

+223
-41
lines changed

5 files changed

+223
-41
lines changed

spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,8 @@
211211
* {@link org.springframework.core.env.Environment Environment} before the
212212
* {@code ApplicationContext} is loaded for the test. All key-value pairs
213213
* will be added to the enclosing {@code Environment} as a single test
214-
* {@code PropertySource} with the highest precedence.
214+
* {@code PropertySource} with the highest precedence. As of Spring Framework
215+
* 6.1, multiple key-value pairs may be specified via a single <em>text block</em>.
215216
* <h4>Supported Syntax</h4>
216217
* <p>The supported syntax for key-value pairs is the same as the
217218
* syntax defined for entries in a Java
@@ -221,6 +222,28 @@
221222
* <li>{@code "key:value"}</li>
222223
* <li>{@code "key value"}</li>
223224
* </ul>
225+
* <h4>Examples</h4>
226+
* <pre class="code">
227+
* &#47;&#47; Using an array of strings
228+
* &#064;TestPropertySource(properties = {
229+
* "key1 = value1",
230+
* "key2 = value2"
231+
* })
232+
* &#064;ContextConfiguration
233+
* class MyTests {
234+
* // ...
235+
* }</pre>
236+
* <pre class="code">
237+
* &#47;&#47; Using a single text block
238+
* &#064;TestPropertySource(properties = """
239+
* key1 = value1
240+
* key2 = value2
241+
* """
242+
* )
243+
* &#064;ContextConfiguration
244+
* class MyTests {
245+
* // ...
246+
* }</pre>
224247
* <h4>Precedence</h4>
225248
* <p>Properties declared via this attribute have higher precedence than
226249
* properties loaded from resource {@link #locations}.

spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -367,18 +367,22 @@ public static void addInlinedPropertiesToEnvironment(ConfigurableEnvironment env
367367

368368
/**
369369
* Convert the supplied <em>inlined properties</em> (in the form of <em>key-value</em>
370-
* pairs) into a map keyed by property name, preserving the ordering of property names
371-
* in the returned map.
372-
* <p>Parsing of the key-value pairs is achieved by converting all pairs
373-
* into <em>virtual</em> properties files in memory and delegating to
370+
* pairs) into a map keyed by property name.
371+
* <p>Parsing of the key-value pairs is achieved by converting all supplied
372+
* strings into <em>virtual</em> properties files in memory and delegating to
374373
* {@link Properties#load(java.io.Reader)} to parse each virtual file.
374+
* <p>Generally speaking, the ordering of property names will be preserved in
375+
* the returned map, analogous to the order in which the key-value pairs are
376+
* supplied to this method. However, if a single string contains multiple
377+
* key-value pairs separated by newlines &mdash; for example, when supplied by
378+
* a user via a <em>text block</em> &mdash; the ordering of property names for
379+
* those particular key-value pairs cannot be guaranteed in the returned map.
375380
* <p>For a full discussion of <em>inlined properties</em>, consult the Javadoc
376381
* for {@link TestPropertySource#properties}.
377382
* @param inlinedProperties the inlined properties to convert; potentially empty
378383
* but never {@code null}
379384
* @return a new, ordered map containing the converted properties
380-
* @throws IllegalStateException if a given key-value pair cannot be parsed, or if
381-
* a given inlined property contains multiple key-value pairs
385+
* @throws IllegalStateException if a given key-value pair cannot be parsed
382386
* @since 4.1.5
383387
* @see #addInlinedPropertiesToEnvironment(ConfigurableEnvironment, String[])
384388
*/
@@ -395,9 +399,8 @@ public static Map<String, Object> convertInlinedPropertiesToMap(String... inline
395399
props.load(new StringReader(pair));
396400
}
397401
catch (Exception ex) {
398-
throw new IllegalStateException("Failed to load test environment property from [" + pair + "]", ex);
402+
throw new IllegalStateException("Failed to load test environment properties from [" + pair + "]", ex);
399403
}
400-
Assert.state(props.size() == 1, () -> "Failed to load exactly one test environment property from [" + pair + "]");
401404
for (String name : props.stringPropertyNames()) {
402405
map.put(name, props.getProperty(name));
403406
}

spring-test/src/test/java/org/springframework/test/context/env/InlinedPropertiesTestPropertySourceTests.java

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -36,6 +36,7 @@
3636
*
3737
* @author Sam Brannen
3838
* @since 4.1
39+
* @see InlinedPropertiesWithTextBlockTestPropertySourceTests
3940
*/
4041
@ExtendWith(SpringExtension.class)
4142
@ContextConfiguration
@@ -44,40 +45,37 @@
4445
class InlinedPropertiesTestPropertySourceTests {
4546

4647
@Autowired
47-
private ConfigurableEnvironment env;
48+
ConfigurableEnvironment env;
4849

4950

50-
private String property(String key) {
51-
return env.getProperty(key);
52-
}
53-
5451
@Test
5552
void propertiesAreAvailableInEnvironment() {
5653
// Simple key/value pairs
57-
assertThat(property("foo")).isEqualTo("bar");
58-
assertThat(property("baz")).isEqualTo("quux");
59-
assertThat(property("enigma")).isEqualTo("42");
54+
assertEnvironmentProperty("foo", "bar");
55+
assertEnvironmentProperty("baz", "quux");
56+
assertEnvironmentProperty("enigma", "42");
6057

6158
// Values containing key/value delimiters (":", "=", " ")
62-
assertThat(property("x.y.z")).isEqualTo("a=b=c");
63-
assertThat(property("server.url")).isEqualTo("https://example.com");
64-
assertThat(property("key.value.1")).isEqualTo("key=value");
65-
assertThat(property("key.value.2")).isEqualTo("key=value");
66-
assertThat(property("key.value.3")).isEqualTo("key:value");
59+
assertEnvironmentProperty("x.y.z", "a=b=c");
60+
assertEnvironmentProperty("server.url", "https://example.com");
61+
assertEnvironmentProperty("key.value.1", "key=value");
62+
assertEnvironmentProperty("key.value.2", "key=value");
63+
assertEnvironmentProperty("key.value.3", "key:value");
6764
}
6865

6966
@Test
7067
@SuppressWarnings("rawtypes")
7168
void propertyNameOrderingIsPreservedInEnvironment() {
72-
final String[] expectedPropertyNames = new String[] { "foo", "baz", "enigma", "x.y.z", "server.url",
73-
"key.value.1", "key.value.2", "key.value.3" };
7469
EnumerablePropertySource eps = (EnumerablePropertySource) env.getPropertySources().get(
7570
INLINED_PROPERTIES_PROPERTY_SOURCE_NAME);
76-
assertThat(eps.getPropertyNames()).isEqualTo(expectedPropertyNames);
71+
assertThat(eps.getPropertyNames()).containsExactly("foo", "baz", "enigma", "x.y.z", "server.url",
72+
"key.value.1", "key.value.2", "key.value.3" );
7773
}
7874

75+
private void assertEnvironmentProperty(String name, Object value) {
76+
assertThat(this.env.getProperty(name)).as("environment property '%s'", name).isEqualTo(value);
77+
}
7978

80-
// -------------------------------------------------------------------
8179

8280
@Configuration
8381
static class Config {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.env;
18+
19+
import org.junit.jupiter.api.Nested;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.api.extension.ExtendWith;
22+
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.context.annotation.Configuration;
25+
import org.springframework.core.env.ConfigurableEnvironment;
26+
import org.springframework.core.env.EnumerablePropertySource;
27+
import org.springframework.core.env.Environment;
28+
import org.springframework.test.annotation.DirtiesContext;
29+
import org.springframework.test.context.ContextConfiguration;
30+
import org.springframework.test.context.TestPropertySource;
31+
import org.springframework.test.context.junit.jupiter.SpringExtension;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.springframework.test.context.support.TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME;
35+
36+
/**
37+
* Integration tests for {@link TestPropertySource @TestPropertySource} support
38+
* with inlined properties supplied via text blocks.
39+
*
40+
* @author Sam Brannen
41+
* @since 6.1
42+
* @see InlinedPropertiesTestPropertySourceTests
43+
*/
44+
@ExtendWith(SpringExtension.class)
45+
@ContextConfiguration
46+
@DirtiesContext
47+
class InlinedPropertiesWithTextBlockTestPropertySourceTests {
48+
49+
@Nested
50+
@DirtiesContext
51+
@TestPropertySource(properties = """
52+
foo = bar
53+
baz quux
54+
enigma: 42
55+
x.y.z = a=b=c
56+
server.url = https://example.com
57+
key.value.1: key=value
58+
key.value.2 key=value
59+
key.value.3 key:value
60+
""")
61+
class AllInOneTextBlockTests {
62+
63+
@Autowired
64+
ConfigurableEnvironment env;
65+
66+
@Test
67+
void propertiesAreAvailableInEnvironment() {
68+
// Simple key/value pairs
69+
assertEnvironmentProperty(this.env, "foo", "bar");
70+
assertEnvironmentProperty(this.env, "baz", "quux");
71+
assertEnvironmentProperty(this.env, "enigma", "42");
72+
73+
// Values containing key/value delimiters (":", "=", " ")
74+
assertEnvironmentProperty(this.env, "x.y.z", "a=b=c");
75+
assertEnvironmentProperty(this.env, "server.url", "https://example.com");
76+
assertEnvironmentProperty(this.env, "key.value.1", "key=value");
77+
assertEnvironmentProperty(this.env, "key.value.2", "key=value");
78+
assertEnvironmentProperty(this.env, "key.value.3", "key:value");
79+
}
80+
81+
/**
82+
* Not necessarily preserved because the properties are all added at the
83+
* same time.
84+
*/
85+
@Test
86+
@SuppressWarnings("rawtypes")
87+
void propertyNameOrderingIsNotNecessarilyPreservedInEnvironment() {
88+
EnumerablePropertySource eps = (EnumerablePropertySource) env.getPropertySources().get(
89+
INLINED_PROPERTIES_PROPERTY_SOURCE_NAME);
90+
assertThat(eps.getPropertyNames()).containsExactlyInAnyOrder("foo", "baz", "enigma", "x.y.z",
91+
"server.url", "key.value.1", "key.value.2", "key.value.3");
92+
}
93+
94+
}
95+
96+
@Nested
97+
@DirtiesContext
98+
@TestPropertySource(properties = {
99+
"""
100+
foo = bar
101+
""",
102+
"""
103+
bar = baz
104+
""",
105+
"""
106+
baz = quux
107+
"""
108+
})
109+
class MultipleTextBlockTests {
110+
111+
@Autowired
112+
ConfigurableEnvironment env;
113+
114+
@Test
115+
void propertiesAreAvailableInEnvironment() {
116+
assertEnvironmentProperty(this.env, "foo", "bar");
117+
assertEnvironmentProperty(this.env, "bar", "baz");
118+
assertEnvironmentProperty(this.env, "baz", "quux");
119+
}
120+
121+
@Test
122+
@SuppressWarnings("rawtypes")
123+
void propertyNameOrderingIsPreservedInEnvironment() {
124+
EnumerablePropertySource eps = (EnumerablePropertySource) env.getPropertySources().get(
125+
INLINED_PROPERTIES_PROPERTY_SOURCE_NAME);
126+
assertThat(eps.getPropertyNames()).containsExactly("foo", "bar", "baz");
127+
}
128+
129+
}
130+
131+
132+
static void assertEnvironmentProperty(Environment env, String name, Object value) {
133+
assertThat(env.getProperty(name)).as("environment property '%s'", name).isEqualTo(value);
134+
}
135+
136+
137+
@Configuration
138+
static class Config {
139+
/* no user beans required for these tests */
140+
}
141+
142+
}

spring-test/src/test/java/org/springframework/test/context/support/TestPropertySourceUtilsTests.java

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.lang.annotation.Retention;
2020
import java.lang.annotation.RetentionPolicy;
2121
import java.util.List;
22-
import java.util.Map;
2322
import java.util.stream.Stream;
2423

2524
import org.assertj.core.api.SoftAssertions;
@@ -28,7 +27,9 @@
2827
import org.springframework.context.ConfigurableApplicationContext;
2928
import org.springframework.core.annotation.AnnotationConfigurationException;
3029
import org.springframework.core.env.ConfigurableEnvironment;
30+
import org.springframework.core.env.MapPropertySource;
3131
import org.springframework.core.env.MutablePropertySources;
32+
import org.springframework.core.env.PropertySource;
3233
import org.springframework.core.io.ByteArrayResource;
3334
import org.springframework.core.io.ResourceLoader;
3435
import org.springframework.core.io.support.PropertySourceDescriptor;
@@ -40,9 +41,11 @@
4041
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
4142
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
4243
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
44+
import static org.assertj.core.api.Assertions.entry;
4345
import static org.mockito.ArgumentMatchers.anyString;
4446
import static org.mockito.BDDMockito.given;
4547
import static org.mockito.Mockito.mock;
48+
import static org.springframework.test.context.support.TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME;
4649
import static org.springframework.test.context.support.TestPropertySourceUtils.addInlinedPropertiesToEnvironment;
4750
import static org.springframework.test.context.support.TestPropertySourceUtils.addPropertiesFilesToEnvironment;
4851
import static org.springframework.test.context.support.TestPropertySourceUtils.buildMergedTestPropertySources;
@@ -58,19 +61,20 @@ class TestPropertySourceUtilsTests {
5861

5962
private static final String[] EMPTY_STRING_ARRAY = new String[0];
6063

61-
private static final String[] KEY_VALUE_PAIR = new String[] {"key = value"};
64+
private static final String[] KEY_VALUE_PAIR = {"key = value"};
6265

63-
private static final String[] FOO_LOCATIONS = new String[] {"classpath:/foo.properties"};
66+
private static final String[] FOO_LOCATIONS = {"classpath:/foo.properties"};
6467

6568

6669
@Test
6770
void emptyAnnotation() {
6871
assertThatIllegalStateException()
6972
.isThrownBy(() -> buildMergedTestPropertySources(EmptyPropertySources.class))
70-
.withMessageStartingWith("Could not detect default properties file for test class")
71-
.withMessageContaining("class path resource")
72-
.withMessageContaining("does not exist")
73-
.withMessageContaining("EmptyPropertySources.properties");
73+
.withMessageContainingAll(
74+
"Could not detect default properties file for test class",
75+
"class path resource",
76+
"does not exist",
77+
"EmptyPropertySources.properties");
7478
}
7579

7680
@Test
@@ -260,16 +264,26 @@ void addInlinedPropertiesToEnvironmentWithEnvironmentAndNullInlinedProperties()
260264

261265
@Test
262266
void addInlinedPropertiesToEnvironmentWithMalformedUnicodeInValue() {
267+
String properties = "key = \\uZZZZ";
263268
assertThatIllegalStateException()
264-
.isThrownBy(() -> addInlinedPropertiesToEnvironment(new MockEnvironment(), asArray("key = \\uZZZZ")))
265-
.withMessageContaining("Failed to load test environment property");
269+
.isThrownBy(() -> addInlinedPropertiesToEnvironment(new MockEnvironment(), properties))
270+
.withMessageContaining("Failed to load test environment properties from [%s]", properties);
266271
}
267272

268273
@Test
269274
void addInlinedPropertiesToEnvironmentWithMultipleKeyValuePairsInSingleInlinedProperty() {
270-
assertThatIllegalStateException()
271-
.isThrownBy(() -> addInlinedPropertiesToEnvironment(new MockEnvironment(), asArray("a=b\nx=y")))
272-
.withMessageContaining("Failed to load exactly one test environment property");
275+
ConfigurableEnvironment environment = new MockEnvironment();
276+
MutablePropertySources propertySources = environment.getPropertySources();
277+
propertySources.remove(MockPropertySource.MOCK_PROPERTIES_PROPERTY_SOURCE_NAME);
278+
assertThat(propertySources).isEmpty();
279+
addInlinedPropertiesToEnvironment(environment, """
280+
a=b
281+
x=y
282+
""");
283+
assertThat(propertySources).hasSize(1);
284+
PropertySource<?> propertySource = propertySources.get(INLINED_PROPERTIES_PROPERTY_SOURCE_NAME);
285+
assertThat(propertySource).isInstanceOf(MapPropertySource.class);
286+
assertThat(((MapPropertySource) propertySource).getSource()).containsExactly(entry("a", "b"), entry("x", "y"));
273287
}
274288

275289
@Test
@@ -279,9 +293,11 @@ void addInlinedPropertiesToEnvironmentWithEmptyProperty() {
279293
MutablePropertySources propertySources = environment.getPropertySources();
280294
propertySources.remove(MockPropertySource.MOCK_PROPERTIES_PROPERTY_SOURCE_NAME);
281295
assertThat(propertySources).isEmpty();
282-
addInlinedPropertiesToEnvironment(environment, asArray(" "));
296+
addInlinedPropertiesToEnvironment(environment, " ");
283297
assertThat(propertySources).hasSize(1);
284-
assertThat(((Map<?, ?>) propertySources.iterator().next().getSource())).isEmpty();
298+
PropertySource<?> propertySource = propertySources.get(INLINED_PROPERTIES_PROPERTY_SOURCE_NAME);
299+
assertThat(propertySource).isInstanceOf(MapPropertySource.class);
300+
assertThat(((MapPropertySource) propertySource).getSource()).isEmpty();
285301
}
286302

287303
@Test

0 commit comments

Comments
 (0)