Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add YamlPropertySourceFactory that can be used for loading YAML files through the @TestPropertySource annotation #42603

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Sources are considered in the following order:
. `properties` attribute on your tests.
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].
. javadoc:{url-spring-framework-javadoc}/org.springframework.test.context.DynamicPropertySource[format=annotation] annotations in your tests.
. javadoc:{url-spring-framework-javadoc}/org.springframework.test.context.TestPropertySource[format=annotation] annotations on your tests.
. 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.
. xref:using/devtools.adoc#using.devtools.globalsettings[Devtools global settings properties] in the `$HOME/.config/spring-boot` directory when devtools is active.

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

WARNING: Multi-document property files cannot be loaded by using the javadoc:org.springframework.context.annotation.PropertySource[format=annotation] or javadoc:org.springframework.test.context.TestPropertySource[format=annotation] annotations.
WARNING: Multi-document property files cannot be loaded by using the javadoc:org.springframework.context.annotation.PropertySource[format=annotation], javadoc:org.springframework.test.context.TestPropertySource[format=annotation] or javadoc:org.springframework.boot.test.context.TestYamlPropertySource[format=annotation] annotations.



Expand Down
2 changes: 1 addition & 1 deletion spring-boot-project/spring-boot-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@ dependencies {
testImplementation("org.testng:testng")

testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
testRuntimeOnly("org.yaml:snakeyaml")
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2012-2024 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.boot.test.context;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.TestPropertySource;

/**
* {@code @TestYamlPropertySource} is an annotation that can be applied to a test class to
* configure the locations of YAML files and inlined properties to be added to the
* Environment's set of PropertySources for an ApplicationContext for integration tests.
* <p>
* Provides a convenient alternative for
* {@code @TestPropertySource(locations = "...", factory = YamlPropertySourceFactory.class)}.
* <p>
* {@code @TestYamlPropertySource} should be considered as {@code @TestPropertySource} but
* for YAML files. It intentionally does not support multi-document YAML files to maintain
* consistency with the behavior of {@code @TestPropertySource}.
*
* @author Dmytro Nosan
* @since 3.5.0
* @see YamlPropertySourceFactory
* @see TestPropertySource
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@TestPropertySource(factory = YamlPropertySourceFactory.class)
@Repeatable(TestYamlPropertySources.class)
public @interface TestYamlPropertySource {

/**
* Alias for {@link TestPropertySource#value()}.
* @return The resource locations of YAML files.
* @see TestPropertySource#value() for more details.
*/
@AliasFor(attribute = "value", annotation = TestPropertySource.class)
String[] value() default {};

/**
* Alias for {@link TestPropertySource#locations()}.
* @return The resource locations of YAML files.
* @see TestPropertySource#locations() for more details.
*/
@AliasFor(attribute = "locations", annotation = TestPropertySource.class)
String[] locations() default {};

/**
* Alias for {@link TestPropertySource#inheritLocations()}.
* @return Whether test property source {@link #locations} from superclasses and
* enclosing classes should be <em>inherited</em>.
* @see TestPropertySource#inheritLocations() for more details.
*/
@AliasFor(attribute = "inheritLocations", annotation = TestPropertySource.class)
boolean inheritLocations() default true;

/**
* Alias for {@link TestPropertySource#properties()}.
* @return <em>Inlined properties</em> in the form of <em>key-value</em> pairs that
* should be added to the Environment
* @see TestPropertySource#properties() for more details.
*/
@AliasFor(attribute = "properties", annotation = TestPropertySource.class)
String[] properties() default {};

/**
* Alias for {@link TestPropertySource#inheritProperties()}.
* @return Whether inlined test {@link #properties} from superclasses and enclosing
* classes should be <em>inherited</em>.
* @see TestPropertySource#inheritProperties() for more details.
*/
@AliasFor(attribute = "inheritProperties", annotation = TestPropertySource.class)
boolean inheritProperties() default true;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2012-2024 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.boot.test.context;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* {@code @TestYamlPropertySources} is a container for one or more
* {@link TestYamlPropertySource @TestYamlPropertySource} declarations.
*
* @author Dmytro Nosan
* @since 3.5.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface TestYamlPropertySources {

/**
* An array of one or more {@link TestYamlPropertySource @TestYamlPropertySource}
* declarations.
* @return {@link TestYamlPropertySource @TestYamlPropertySource} annotations.
*/
TestYamlPropertySource[] value();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2012-2024 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.boot.test.context;

import java.io.IOException;
import java.util.Collections;
import java.util.List;

import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
* An implementation of {@link PropertySourceFactory} that delegates the loading of
* {@code PropertySource} to {@link YamlPropertySourceLoader}.
* <p>
* Even though {@link YamlPropertySourceLoader} supports multi-document YAML files, the
* {@code YamlPropertySourceFactory} intentionally does not allow this.
*
* @author Dmytro Nosan
* @since 3.5.0
* @see TestYamlPropertySource
*/
public class YamlPropertySourceFactory implements PropertySourceFactory {

private static final YamlPropertySourceLoader loader = new YamlPropertySourceLoader();

@Override
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) throws IOException {
Resource resource = encodedResource.getResource();
String propertySourceName = getPropertySourceName(name, resource);
List<PropertySource<?>> propertySources = loader.load(propertySourceName, resource);
Assert.isTrue(propertySources.size() <= 1, () -> resource + " is a multi-document YAML file");
if (propertySources.isEmpty()) {
return new MapPropertySource(name, Collections.emptyMap());
}
return propertySources.get(0);
}

private static String getPropertySourceName(String name, Resource resource) {
if (StringUtils.hasText(name)) {
return name;
}
String description = resource.getDescription();
if (StringUtils.hasText(description)) {
return description;
}
return resource.getClass().getSimpleName() + "@" + System.identityHashCode(resource);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2012-2024 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.boot.test.context;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Integration tests for {@link YamlPropertySourceFactory} with
* {@link TestYamlPropertySource}.
*
* @author Dmytro Nosan
*/
@SpringJUnitConfig
@TestYamlPropertySource({ "test.yaml", "test1.yaml" })
@TestYamlPropertySource(locations = "test2.yaml", properties = "key:value")
class TestYamlPropertySourceIntegrationTests {

@Autowired
private Environment environment;

@Test
void loadProperties() {
assertThat(this.environment.getProperty("spring.bar")).isEqualTo("bar");
assertThat(this.environment.getProperty("spring.foo")).isEqualTo("baz");
assertThat(this.environment.getProperty("spring.buzz")).isEqualTo("fazz");
assertThat(this.environment.getProperty("spring.boot")).isEqualTo("boot");
assertThat(this.environment.getProperty("key")).isEqualTo("value");
}

@Configuration(proxyBeanMethods = false)
static class Config {

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2012-2024 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.boot.test.context;

import java.io.IOException;

import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;

import org.springframework.core.env.PropertySource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.EncodedResource;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.spy;

/**
* Tests for {@link YamlPropertySourceFactory}.
*
* @author Dmytro Nosan
*/
class YamlPropertySourceFactoryTests {

private final YamlPropertySourceFactory factory = new YamlPropertySourceFactory();

@Test
void shouldCreatePropertySourceWithGivenName() throws IOException {
EncodedResource resource = new EncodedResource(create("test.yaml"));
PropertySource<?> propertySource = this.factory.createPropertySource("test", resource);
assertThat(propertySource.getName()).isEqualTo("test");
assertProperties(propertySource);
}

@Test
void shouldCreatePropertySourceWithResourceDescriptionName() throws IOException {
EncodedResource resource = new EncodedResource(create("test.yaml"));
PropertySource<?> propertySource = this.factory.createPropertySource(null, resource);
assertThat(propertySource.getName()).isEqualTo(resource.getResource().getDescription());
assertProperties(propertySource);
}

@Test
void shouldCreatePropertySourceWithGeneratedName() throws IOException {
Resource resource = spy(create("test.yaml"));
willReturn(null).given(resource).getDescription();
PropertySource<?> propertySource = this.factory.createPropertySource(null, new EncodedResource(resource));
assertThat(propertySource.getName()).startsWith("ClassPathResource@");
assertProperties(propertySource);
}

@Test
void shouldNotCreatePropertySourceWhenMultiDocumentYaml() {
EncodedResource resource = new EncodedResource(create("multi.yaml"));
assertThatIllegalArgumentException().isThrownBy(() -> this.factory.createPropertySource(null, resource))
.withMessageContaining("is a multi-document YAML file");
}

@Test
void shouldCreateEmptyPropertySourceWhenYamlFileIsEmpty() throws IOException {
EncodedResource resource = new EncodedResource(create("empty.yaml"));
PropertySource<?> propertySource = this.factory.createPropertySource("empty", resource);
assertThat(propertySource.getName()).isEqualTo("empty");
assertThat(propertySource.getSource()).asInstanceOf(InstanceOfAssertFactories.map(String.class, Object.class))
.isEmpty();
}

private Resource create(String name) {
return new ClassPathResource(name, getClass());
}

private static void assertProperties(PropertySource<?> propertySource) {
assertThat(propertySource.getProperty("spring.bar")).isEqualTo("bar");
assertThat(propertySource.getProperty("spring.foo")).isEqualTo("baz");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
spring:
bar: bar
foo: baz
---
spring:
foo: baz
Loading
Loading