Skip to content

Commit e5d41d9

Browse files
committed
Preserve ordering of inlined props in @TestPropertySource
The initial implementation for adding inlined properties configured via @TestPropertySource to the context's environment did not preserve the order in which the properties were physically declared. This makes @TestPropertySource a poor testing facility for mimicking the production environment's configuration if the property source mechanism used in production preserves ordering of property names -- which is the case for YAML-based property sources used in Spring Boot, Spring Yarn, etc. This commit addresses this issue by ensuring that the ordering of inlined properties declared via @TestPropertySource is preserved. Specifically, the original functionality has been refactored. extracted from AbstractContextLoader, and moved to TestPropertySourceUtils where it may later be made public for general purpose use in other frameworks. Issue: SPR-12710 (cherry picked from commit d6a799a)
1 parent f398dd0 commit e5d41d9

File tree

4 files changed

+170
-89
lines changed

4 files changed

+170
-89
lines changed

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

Lines changed: 3 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 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.
@@ -16,13 +16,8 @@
1616

1717
package org.springframework.test.context.support;
1818

19-
import java.io.IOException;
20-
import java.io.StringReader;
2119
import java.util.ArrayList;
22-
import java.util.HashMap;
2320
import java.util.List;
24-
import java.util.Map;
25-
import java.util.Properties;
2621
import java.util.Set;
2722

2823
import org.apache.commons.logging.Log;
@@ -34,12 +29,8 @@
3429
import org.springframework.context.ConfigurableApplicationContext;
3530
import org.springframework.core.GenericTypeResolver;
3631
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
37-
import org.springframework.core.env.ConfigurableEnvironment;
38-
import org.springframework.core.env.MapPropertySource;
3932
import org.springframework.core.env.PropertySource;
4033
import org.springframework.core.io.ClassPathResource;
41-
import org.springframework.core.io.Resource;
42-
import org.springframework.core.io.support.ResourcePropertySource;
4334
import org.springframework.test.context.ContextConfigurationAttributes;
4435
import org.springframework.test.context.ContextLoader;
4536
import org.springframework.test.context.MergedContextConfiguration;
@@ -64,7 +55,6 @@
6455
*
6556
* @author Sam Brannen
6657
* @author Juergen Hoeller
67-
* @author Dave Syer
6858
* @since 2.5
6959
* @see #generateDefaultLocations
7060
* @see #getResourceSuffixes
@@ -74,8 +64,6 @@ public abstract class AbstractContextLoader implements SmartContextLoader {
7464

7565
private static final String[] EMPTY_STRING_ARRAY = new String[0];
7666

77-
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
78-
7967
private static final Log logger = LogFactory.getLog(AbstractContextLoader.class);
8068

8169

@@ -137,74 +125,11 @@ public void processContextConfiguration(ContextConfigurationAttributes configAtt
137125
*/
138126
protected void prepareContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
139127
context.getEnvironment().setActiveProfiles(mergedConfig.getActiveProfiles());
140-
addResourcePropertySourcesToEnvironment(context, mergedConfig);
141-
addInlinedPropertiesToEnvironment(context, mergedConfig);
128+
TestPropertySourceUtils.addResourcePropertySourcesToEnvironment(context, mergedConfig.getPropertySourceLocations());
129+
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, mergedConfig.getPropertySourceProperties());
142130
invokeApplicationContextInitializers(context, mergedConfig);
143131
}
144132

145-
/**
146-
* @since 4.1
147-
*/
148-
private void addResourcePropertySourcesToEnvironment(ConfigurableApplicationContext context,
149-
MergedContextConfiguration mergedConfig) {
150-
try {
151-
ConfigurableEnvironment environment = context.getEnvironment();
152-
String[] locations = mergedConfig.getPropertySourceLocations();
153-
for (String location : locations) {
154-
String resolvedLocation = environment.resolveRequiredPlaceholders(location);
155-
Resource resource = context.getResource(resolvedLocation);
156-
ResourcePropertySource ps = new ResourcePropertySource(resource);
157-
environment.getPropertySources().addFirst(ps);
158-
}
159-
}
160-
catch (IOException e) {
161-
throw new IllegalStateException("Failed to add PropertySource to Environment", e);
162-
}
163-
}
164-
165-
/**
166-
* @since 4.1
167-
*/
168-
private void addInlinedPropertiesToEnvironment(ConfigurableApplicationContext context,
169-
MergedContextConfiguration mergedConfig) {
170-
String[] keyValuePairs = mergedConfig.getPropertySourceProperties();
171-
if (!ObjectUtils.isEmpty(keyValuePairs)) {
172-
String name = "test properties " + ObjectUtils.nullSafeToString(keyValuePairs);
173-
MapPropertySource ps = new MapPropertySource(name, extractEnvironmentProperties(keyValuePairs));
174-
context.getEnvironment().getPropertySources().addFirst(ps);
175-
}
176-
}
177-
178-
/**
179-
* Extract environment properties from the supplied key/value pairs.
180-
* <p>Parsing of the key/value pairs is achieved by converting all pairs
181-
* into a single <em>virtual</em> properties file in memory and delegating
182-
* to {@link Properties#load(java.io.Reader)} to parse that virtual file.
183-
* <p>This code has been adapted from Spring Boot's
184-
* {@link org.springframework.boot.test.SpringApplicationContextLoader SpringApplicationContextLoader}.
185-
* @since 4.1
186-
*/
187-
private Map<String, Object> extractEnvironmentProperties(String[] keyValuePairs) {
188-
StringBuilder sb = new StringBuilder();
189-
for (String keyValuePair : keyValuePairs) {
190-
sb.append(keyValuePair).append(LINE_SEPARATOR);
191-
}
192-
String content = sb.toString();
193-
Properties props = new Properties();
194-
try {
195-
props.load(new StringReader(content));
196-
}
197-
catch (IOException e) {
198-
throw new IllegalStateException("Failed to load test environment properties from: " + content, e);
199-
}
200-
201-
Map<String, Object> properties = new HashMap<String, Object>();
202-
for (String name : props.stringPropertyNames()) {
203-
properties.put(name, props.getProperty(name));
204-
}
205-
return properties;
206-
}
207-
208133
@SuppressWarnings("unchecked")
209134
private void invokeApplicationContextInitializers(ConfigurableApplicationContext context,
210135
MergedContextConfiguration mergedConfig) {

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

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 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.
@@ -16,24 +16,39 @@
1616

1717
package org.springframework.test.context.support;
1818

19+
import java.io.IOException;
20+
import java.io.StringReader;
1921
import java.util.ArrayList;
2022
import java.util.Arrays;
23+
import java.util.LinkedHashMap;
2124
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Properties;
2227

2328
import org.apache.commons.logging.Log;
2429
import org.apache.commons.logging.LogFactory;
2530

31+
import org.springframework.context.ConfigurableApplicationContext;
2632
import org.springframework.core.annotation.AnnotationAttributes;
33+
import org.springframework.core.env.ConfigurableEnvironment;
34+
import org.springframework.core.env.MapPropertySource;
35+
import org.springframework.core.env.PropertySource;
36+
import org.springframework.core.io.Resource;
37+
import org.springframework.core.io.support.ResourcePropertySource;
2738
import org.springframework.test.context.TestPropertySource;
2839
import org.springframework.test.context.util.TestContextResourceUtils;
29-
import org.springframework.test.util.MetaAnnotationUtils.*;
40+
import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor;
3041
import org.springframework.util.Assert;
42+
import org.springframework.util.ObjectUtils;
3143
import org.springframework.util.StringUtils;
3244

3345
import static org.springframework.test.util.MetaAnnotationUtils.*;
3446

3547
/**
36-
* Utility methods for working with {@link TestPropertySource @TestPropertySource}.
48+
* Utility methods for working with {@link TestPropertySource @TestPropertySource}
49+
* and adding test {@link PropertySource PropertySources} to the {@code Environment}.
50+
*
51+
* <p>Primarily intended for use within the framework.
3752
*
3853
* @author Sam Brannen
3954
* @since 4.1
@@ -131,4 +146,76 @@ private static String[] mergeProperties(List<TestPropertySourceAttributes> attri
131146
return StringUtils.toStringArray(properties);
132147
}
133148

149+
/**
150+
* @since 4.1.5
151+
*/
152+
static void addResourcePropertySourcesToEnvironment(ConfigurableApplicationContext context,
153+
String[] propertySourceLocations) {
154+
try {
155+
ConfigurableEnvironment environment = context.getEnvironment();
156+
String[] locations = propertySourceLocations;
157+
for (String location : locations) {
158+
String resolvedLocation = environment.resolveRequiredPlaceholders(location);
159+
Resource resource = context.getResource(resolvedLocation);
160+
ResourcePropertySource ps = new ResourcePropertySource(resource);
161+
environment.getPropertySources().addFirst(ps);
162+
}
163+
}
164+
catch (IOException e) {
165+
throw new IllegalStateException("Failed to add PropertySource to Environment", e);
166+
}
167+
}
168+
169+
/**
170+
* @since 4.1.5
171+
*/
172+
static void addInlinedPropertiesToEnvironment(ConfigurableApplicationContext context,
173+
String[] propertySourceProperties) {
174+
addInlinedPropertiesToEnvironment(context.getEnvironment(), propertySourceProperties);
175+
}
176+
177+
/**
178+
* @since 4.1.5
179+
*/
180+
static void addInlinedPropertiesToEnvironment(ConfigurableEnvironment environment, String[] propertySourceProperties) {
181+
if (!ObjectUtils.isEmpty(propertySourceProperties)) {
182+
String name = "test properties " + ObjectUtils.nullSafeToString(propertySourceProperties);
183+
MapPropertySource ps = new MapPropertySource(name, extractEnvironmentProperties(propertySourceProperties));
184+
environment.getPropertySources().addFirst(ps);
185+
}
186+
}
187+
188+
/**
189+
* Extract environment properties from the supplied key/value pairs,
190+
* preserving the ordering of property names in the returned map.
191+
* <p>Parsing of the key/value pairs is achieved by converting all pairs
192+
* into <em>virtual</em> properties files in memory and delegating to
193+
* {@link Properties#load(java.io.Reader)} to parse each virtual file.
194+
*/
195+
private static Map<String, Object> extractEnvironmentProperties(String[] keyValuePairs) {
196+
Map<String, Object> map = new LinkedHashMap<String, Object>();
197+
198+
Properties props = new Properties();
199+
for (String pair : keyValuePairs) {
200+
if (!StringUtils.hasText(pair)) {
201+
continue;
202+
}
203+
204+
try {
205+
props.load(new StringReader(pair));
206+
}
207+
catch (Exception e) {
208+
throw new IllegalStateException("Failed to load test environment property from [" + pair + "].", e);
209+
}
210+
Assert.state(props.size() == 1, "Failed to load exactly one test environment property from [" + pair + "].");
211+
212+
for (String name : props.stringPropertyNames()) {
213+
map.put(name, props.getProperty(name));
214+
}
215+
props.clear();
216+
}
217+
218+
return map;
219+
}
220+
134221
}

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

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,43 +21,71 @@
2121

2222
import org.springframework.beans.factory.annotation.Autowired;
2323
import org.springframework.context.annotation.Configuration;
24+
import org.springframework.core.env.ConfigurableEnvironment;
25+
import org.springframework.core.env.EnumerablePropertySource;
2426
import org.springframework.core.env.Environment;
27+
import org.springframework.core.env.PropertySource;
2528
import org.springframework.test.context.ContextConfiguration;
2629
import org.springframework.test.context.TestPropertySource;
2730
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
2831

2932
import static org.junit.Assert.*;
3033

3134
/**
32-
* Integration tests for {@link TestPropertySource @TestPropertySource}
33-
* support with an inlined properties.
35+
* Integration tests for {@link TestPropertySource @TestPropertySource} support with
36+
* inlined properties.
3437
*
3538
* @author Sam Brannen
3639
* @since 4.1
3740
*/
3841
@RunWith(SpringJUnit4ClassRunner.class)
3942
@ContextConfiguration
40-
@TestPropertySource(properties = { "foo = bar", "baz quux", "enigma: 42",
41-
"x.y.z = a=b=c", "server.url = http://example.com", "key.value.1: key=value",
42-
"key.value.2 key=value", "key.value.3 key:value" })
43+
@TestPropertySource(properties = { "", "foo = bar", "baz quux", "enigma: 42", "x.y.z = a=b=c",
44+
"server.url = http://example.com", "key.value.1: key=value", "key.value.2 key=value", "key.value.3 key:value" })
4345
public class InlinedPropertiesTestPropertySourceTests {
4446

4547
@Autowired
46-
protected Environment env;
48+
private Environment env;
4749

4850

4951
@Test
50-
public void verifyPropertiesAreAvailableInEnvironment() {
52+
public void propertiesAreAvailableInEnvironment() {
53+
54+
// Simple key/value pairs
5155
assertEquals("bar", env.getProperty("foo"));
5256
assertEquals("quux", env.getProperty("baz"));
5357
assertEquals(42, env.getProperty("enigma", Integer.class).intValue());
58+
59+
// Values containing key/value delimiters (":", "=", " ")
5460
assertEquals("a=b=c", env.getProperty("x.y.z"));
5561
assertEquals("http://example.com", env.getProperty("server.url"));
5662
assertEquals("key=value", env.getProperty("key.value.1"));
5763
assertEquals("key=value", env.getProperty("key.value.2"));
5864
assertEquals("key:value", env.getProperty("key.value.3"));
5965
}
6066

67+
@Test
68+
@SuppressWarnings("rawtypes")
69+
public void propertyNameOrderingIsPreservedInEnvironment() {
70+
String[] propertyNames = null;
71+
72+
ConfigurableEnvironment configurableEnvironment = (ConfigurableEnvironment) env;
73+
for (PropertySource<?> propertySource : configurableEnvironment.getPropertySources()) {
74+
if (propertySource instanceof EnumerablePropertySource) {
75+
EnumerablePropertySource eps = (EnumerablePropertySource) propertySource;
76+
if (eps.getName().startsWith("test properties")) {
77+
propertyNames = eps.getPropertyNames();
78+
break;
79+
}
80+
}
81+
}
82+
83+
final String[] expectedPropertyNames = new String[] { "foo", "baz", "enigma", "x.y.z", "server.url",
84+
"key.value.1", "key.value.2", "key.value.3" };
85+
86+
assertArrayEquals(expectedPropertyNames, propertyNames);
87+
}
88+
6189

6290
// -------------------------------------------------------------------
6391

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 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.
@@ -16,10 +16,16 @@
1616

1717
package org.springframework.test.context.support;
1818

19+
import java.util.Map;
20+
1921
import org.junit.Rule;
2022
import org.junit.Test;
2123
import org.junit.rules.ExpectedException;
2224

25+
import org.springframework.core.env.ConfigurableEnvironment;
26+
import org.springframework.core.env.MutablePropertySources;
27+
import org.springframework.mock.env.MockEnvironment;
28+
import org.springframework.mock.env.MockPropertySource;
2329
import org.springframework.test.context.TestPropertySource;
2430

2531
import static org.hamcrest.CoreMatchers.*;
@@ -113,6 +119,41 @@ public void overriddenLocationsAndProperties() {
113119
new String[] { "classpath:/baz.properties" }, new String[] { "key = value" });
114120
}
115121

122+
/**
123+
* @since 4.1.5
124+
*/
125+
@Test
126+
@SuppressWarnings("rawtypes")
127+
public void emptyInlinedProperty() {
128+
ConfigurableEnvironment environment = new MockEnvironment();
129+
MutablePropertySources propertySources = environment.getPropertySources();
130+
propertySources.remove(MockPropertySource.MOCK_PROPERTIES_PROPERTY_SOURCE_NAME);
131+
assertEquals(0, propertySources.size());
132+
addInlinedPropertiesToEnvironment(environment, new String[] { " " });
133+
assertEquals(1, propertySources.size());
134+
assertEquals(0, ((Map) propertySources.iterator().next().getSource()).size());
135+
}
136+
137+
/**
138+
* @since 4.1.5
139+
*/
140+
@Test
141+
public void inlinedPropertyWithMalformedUnicodeInValue() {
142+
expectedException.expect(IllegalStateException.class);
143+
expectedException.expectMessage("Failed to load test environment property");
144+
addInlinedPropertiesToEnvironment(new MockEnvironment(), new String[] { "key = \\uZZZZ" });
145+
}
146+
147+
/**
148+
* @since 4.1.5
149+
*/
150+
@Test
151+
public void inlinedPropertyWithMultipleKeyValuePairs() {
152+
expectedException.expect(IllegalStateException.class);
153+
expectedException.expectMessage("Failed to load exactly one test environment property");
154+
addInlinedPropertiesToEnvironment(new MockEnvironment(), new String[] { "a=b\nx=y" });
155+
}
156+
116157

117158
// -------------------------------------------------------------------
118159

0 commit comments

Comments
 (0)