Skip to content

Commit b47e010

Browse files
committed
Add YamlPropertySourceFactory and @TestYamlPropertySource that can be applied to a test class to add PropertySource loaded from YAML files to the Environment
This commit introduces new functionality to load properties from YAML files in tests. The @TestYamlPropertySource annotation is added to allow test classes to specify YAML files as property sources. The @TestYamlPropertySource provides a convenient alternative for @TestPropertySource(factory=YamlPropertySourceFactory.class). The YamlPropertySourceFactory class is also introduced to enable the loading of YAML files into the Environment through @TestPropertySource.
1 parent 9f6b25e commit b47e010

File tree

12 files changed

+379
-3
lines changed

12 files changed

+379
-3
lines changed

Diff for: spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/external-config.adoc

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Sources are considered in the following order:
2626
. `properties` attribute on your tests.
2727
Available on javadoc:org.springframework.boot.test.context.SpringBootTest[format=annotation] and the xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.autoconfigured-tests[test annotations for testing a particular slice of your application].
2828
. javadoc:{url-spring-framework-javadoc}/org.springframework.test.context.DynamicPropertySource[format=annotation] annotations in your tests.
29-
. javadoc:{url-spring-framework-javadoc}/org.springframework.test.context.TestPropertySource[format=annotation] annotations on your tests.
29+
. javadoc:{url-spring-framework-javadoc}/org.springframework.test.context.TestPropertySource[format=annotation] or javadoc:org.springframework.boot.test.context.TestYamlPropertySource[format=annotation] annotations on your tests.
3030
. xref:using/devtools.adoc#using.devtools.globalsettings[Devtools global settings properties] in the `$HOME/.config/spring-boot` directory when devtools is active.
3131

3232
Config data files are considered in the following order:
@@ -525,7 +525,7 @@ The lines immediately before and after the separator must not be same comment pr
525525
TIP: Multi-document property files are often used in conjunction with activation properties such as `spring.config.activate.on-profile`.
526526
See the xref:features/external-config.adoc#features.external-config.files.activation-properties[next section] for details.
527527

528-
WARNING: Multi-document property files cannot be loaded by using the `@PropertySource` or `@TestPropertySource` annotations.
528+
WARNING: Multi-document property files cannot be loaded by using the `@PropertySource`, `@TestPropertySource` or `@TestYamlPropertySource` annotations.
529529

530530

531531

Diff for: spring-boot-project/spring-boot-test/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,5 @@ dependencies {
5959
testImplementation("org.testng:testng")
6060

6161
testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
62+
testRuntimeOnly("org.yaml:snakeyaml")
6263
}
63-
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2012-2024 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.boot.test.context;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Inherited;
22+
import java.lang.annotation.Repeatable;
23+
import java.lang.annotation.Retention;
24+
import java.lang.annotation.RetentionPolicy;
25+
import java.lang.annotation.Target;
26+
27+
import org.springframework.core.annotation.AliasFor;
28+
import org.springframework.test.context.TestPropertySource;
29+
30+
/**
31+
* {@code @TestYamlPropertySource} is an annotation that can be applied to a test class to
32+
* configure the locations of YAML files and inlined properties to be added to the
33+
* Environment's set of PropertySources for an ApplicationContext for integration tests.
34+
* <p>
35+
* Provides a convenient alternative for
36+
* {@code @TestPropertySource(locations = "...", factory = YamlPropertySourceFactory.class)}.
37+
* <p>
38+
* {@code @TestYamlPropertySource} should be considered as {@code @TestPropertySource} but
39+
* for YAML files. It intentionally does not support multi-document YAML files to maintain
40+
* consistency with the behavior of {@code @TestPropertySource}.
41+
*
42+
* @author Dmytro Nosan
43+
* @since 3.5.0
44+
* @see YamlPropertySourceFactory
45+
* @see TestPropertySource
46+
*/
47+
@Target(ElementType.TYPE)
48+
@Retention(RetentionPolicy.RUNTIME)
49+
@Documented
50+
@Inherited
51+
@TestPropertySource(factory = YamlPropertySourceFactory.class)
52+
@Repeatable(TestYamlPropertySources.class)
53+
public @interface TestYamlPropertySource {
54+
55+
/**
56+
* Alias for {@link TestPropertySource#value()}.
57+
* @return The resource locations of YAML files.
58+
* @see TestPropertySource#value() for more details.
59+
*/
60+
@AliasFor(attribute = "value", annotation = TestPropertySource.class)
61+
String[] value() default {};
62+
63+
/**
64+
* Alias for {@link TestPropertySource#locations()}.
65+
* @return The resource locations of YAML files.
66+
* @see TestPropertySource#locations() for more details.
67+
*/
68+
@AliasFor(attribute = "locations", annotation = TestPropertySource.class)
69+
String[] locations() default {};
70+
71+
/**
72+
* Alias for {@link TestPropertySource#inheritLocations()}.
73+
* @return Whether test property source {@link #locations} from superclasses and
74+
* enclosing classes should be <em>inherited</em>.
75+
* @see TestPropertySource#inheritLocations() for more details.
76+
*/
77+
@AliasFor(attribute = "inheritLocations", annotation = TestPropertySource.class)
78+
boolean inheritLocations() default true;
79+
80+
/**
81+
* Alias for {@link TestPropertySource#properties()}.
82+
* @return <em>Inlined properties</em> in the form of <em>key-value</em> pairs that
83+
* should be added to the Environment
84+
* @see TestPropertySource#properties() for more details.
85+
*/
86+
@AliasFor(attribute = "properties", annotation = TestPropertySource.class)
87+
String[] properties() default {};
88+
89+
/**
90+
* Alias for {@link TestPropertySource#inheritProperties()}.
91+
* @return Whether inlined test {@link #properties} from superclasses and enclosing
92+
* classes should be <em>inherited</em>.
93+
* @see TestPropertySource#inheritProperties() for more details.
94+
*/
95+
@AliasFor(attribute = "inheritProperties", annotation = TestPropertySource.class)
96+
boolean inheritProperties() default true;
97+
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2012-2024 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.boot.test.context;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Inherited;
22+
import java.lang.annotation.Retention;
23+
import java.lang.annotation.RetentionPolicy;
24+
import java.lang.annotation.Target;
25+
26+
/**
27+
* {@code @TestYamlPropertySources} is a container for one or more
28+
* {@link TestYamlPropertySource @TestYamlPropertySource} declarations.
29+
*
30+
* @author Dmytro Nosan
31+
* @since 3.5.0
32+
*/
33+
@Target(ElementType.TYPE)
34+
@Retention(RetentionPolicy.RUNTIME)
35+
@Documented
36+
@Inherited
37+
public @interface TestYamlPropertySources {
38+
39+
/**
40+
* An array of one or more {@link TestYamlPropertySource @TestYamlPropertySource}
41+
* declarations.
42+
* @return {@link TestYamlPropertySource @TestYamlPropertySource} annotations.
43+
*/
44+
TestYamlPropertySource[] value();
45+
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2012-2024 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.boot.test.context;
18+
19+
import java.io.IOException;
20+
import java.util.Collections;
21+
import java.util.List;
22+
23+
import org.springframework.boot.env.YamlPropertySourceLoader;
24+
import org.springframework.core.env.MapPropertySource;
25+
import org.springframework.core.env.PropertySource;
26+
import org.springframework.core.io.Resource;
27+
import org.springframework.core.io.support.EncodedResource;
28+
import org.springframework.core.io.support.PropertySourceFactory;
29+
import org.springframework.util.Assert;
30+
import org.springframework.util.StringUtils;
31+
32+
/**
33+
* An implementation of {@link PropertySourceFactory} that delegates the loading of
34+
* {@code PropertySource} to {@link YamlPropertySourceLoader}.
35+
* <p>
36+
* Even though {@link YamlPropertySourceLoader} supports multi-document YAML files, the
37+
* {@code YamlPropertySourceFactory} intentionally does not allow this.
38+
*
39+
* @author Dmytro Nosan
40+
* @since 3.5.0
41+
* @see TestYamlPropertySource
42+
*/
43+
public class YamlPropertySourceFactory implements PropertySourceFactory {
44+
45+
private static final YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
46+
47+
@Override
48+
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) throws IOException {
49+
Resource resource = encodedResource.getResource();
50+
String propertySourceName = getPropertySourceName(name, resource);
51+
List<PropertySource<?>> propertySources = loader.load(propertySourceName, resource);
52+
Assert.isTrue(propertySources.size() <= 1, () -> resource + " is a multi-document YAML file");
53+
if (propertySources.isEmpty()) {
54+
return new MapPropertySource(name, Collections.emptyMap());
55+
}
56+
return propertySources.get(0);
57+
}
58+
59+
private static String getPropertySourceName(String name, Resource resource) {
60+
if (StringUtils.hasText(name)) {
61+
return name;
62+
}
63+
String description = resource.getDescription();
64+
if (StringUtils.hasText(description)) {
65+
return description;
66+
}
67+
return resource.getClass().getSimpleName() + "@" + System.identityHashCode(resource);
68+
}
69+
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2012-2024 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.boot.test.context;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.beans.factory.annotation.Autowired;
22+
import org.springframework.context.annotation.Configuration;
23+
import org.springframework.core.env.Environment;
24+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
28+
/**
29+
* Integration tests for {@link YamlPropertySourceFactory} with
30+
* {@link TestYamlPropertySource}.
31+
*
32+
* @author Dmytro Nosan
33+
*/
34+
@SpringJUnitConfig
35+
@TestYamlPropertySource({ "test.yaml", "test1.yaml" })
36+
@TestYamlPropertySource(locations = "test2.yaml", properties = "key:value")
37+
class TestYamlPropertySourceIntegrationTests {
38+
39+
@Autowired
40+
private Environment environment;
41+
42+
@Test
43+
void loadProperties() {
44+
assertThat(this.environment.getProperty("spring.bar")).isEqualTo("bar");
45+
assertThat(this.environment.getProperty("spring.foo")).isEqualTo("baz");
46+
assertThat(this.environment.getProperty("spring.buzz")).isEqualTo("fazz");
47+
assertThat(this.environment.getProperty("spring.boot")).isEqualTo("boot");
48+
assertThat(this.environment.getProperty("key")).isEqualTo("value");
49+
}
50+
51+
@Configuration(proxyBeanMethods = false)
52+
static class Config {
53+
54+
}
55+
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2012-2024 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.boot.test.context;
18+
19+
import java.io.IOException;
20+
21+
import org.assertj.core.api.InstanceOfAssertFactories;
22+
import org.junit.jupiter.api.Test;
23+
24+
import org.springframework.core.env.PropertySource;
25+
import org.springframework.core.io.ClassPathResource;
26+
import org.springframework.core.io.Resource;
27+
import org.springframework.core.io.support.EncodedResource;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
31+
import static org.mockito.BDDMockito.willReturn;
32+
import static org.mockito.Mockito.spy;
33+
34+
/**
35+
* Tests for {@link YamlPropertySourceFactory}.
36+
*
37+
* @author Dmytro Nosan
38+
*/
39+
class YamlPropertySourceFactoryTests {
40+
41+
private final YamlPropertySourceFactory factory = new YamlPropertySourceFactory();
42+
43+
@Test
44+
void shouldCreatePropertySourceWithGivenName() throws IOException {
45+
EncodedResource resource = new EncodedResource(create("test.yaml"));
46+
PropertySource<?> propertySource = this.factory.createPropertySource("test", resource);
47+
assertThat(propertySource.getName()).isEqualTo("test");
48+
assertProperties(propertySource);
49+
}
50+
51+
@Test
52+
void shouldCreatePropertySourceWithResourceDescriptionName() throws IOException {
53+
EncodedResource resource = new EncodedResource(create("test.yaml"));
54+
PropertySource<?> propertySource = this.factory.createPropertySource(null, resource);
55+
assertThat(propertySource.getName()).isEqualTo(resource.getResource().getDescription());
56+
assertProperties(propertySource);
57+
}
58+
59+
@Test
60+
void shouldCreatePropertySourceWithGeneratedName() throws IOException {
61+
Resource resource = spy(create("test.yaml"));
62+
willReturn(null).given(resource).getDescription();
63+
PropertySource<?> propertySource = this.factory.createPropertySource(null, new EncodedResource(resource));
64+
assertThat(propertySource.getName()).startsWith("ClassPathResource@");
65+
assertProperties(propertySource);
66+
}
67+
68+
@Test
69+
void shouldNotCreatePropertySourceWhenMultiDocumentYaml() {
70+
EncodedResource resource = new EncodedResource(create("multi.yaml"));
71+
assertThatIllegalArgumentException().isThrownBy(() -> this.factory.createPropertySource(null, resource))
72+
.withMessageContaining("is a multi-document YAML file");
73+
}
74+
75+
@Test
76+
void shouldCreateEmptyPropertySourceWhenYamlFileIsEmpty() throws IOException {
77+
EncodedResource resource = new EncodedResource(create("empty.yaml"));
78+
PropertySource<?> propertySource = this.factory.createPropertySource("empty", resource);
79+
assertThat(propertySource.getName()).isEqualTo("empty");
80+
assertThat(propertySource.getSource()).asInstanceOf(InstanceOfAssertFactories.map(String.class, Object.class))
81+
.isEmpty();
82+
}
83+
84+
private Resource create(String name) {
85+
return new ClassPathResource(name, getClass());
86+
}
87+
88+
private static void assertProperties(PropertySource<?> propertySource) {
89+
assertThat(propertySource.getProperty("spring.bar")).isEqualTo("bar");
90+
assertThat(propertySource.getProperty("spring.foo")).isEqualTo("baz");
91+
}
92+
93+
}

Diff for: spring-boot-project/spring-boot-test/src/test/resources/org/springframework/boot/test/context/empty.yaml

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
spring:
2+
bar: bar
3+
foo: baz
4+
---
5+
spring:
6+
foo: baz

0 commit comments

Comments
 (0)