Support custom properties file formats in @TestPropertySource

Spring Framework 4.3 introduced the `PropertySourceFactory` SPI for use
with `@PropertySource` on `@Configuration` classes; however, prior to
this commit there was no mechanism to support custom properties file
formats in `@TestPropertySource` for integration tests.

This commit introduces support for configuring a custom
`PropertySourceFactory` via a new `factory` attribute in
`@TestPropertySource` in order to support custom file formats such as
JSON, YAML, etc.

For example, if you create a YamlPropertySourceFactory, you can use it
in integration tests as follows.

@SpringJUnitConfig
@TestPropertySource(locations = "/test.yaml", factory = YamlPropertySourceFactory.class)
class MyTestClass { /* ... /* }

If a custom factory is not specified, traditional `*.properties` and
`*.xml` based `java.util.Properties` file formats are supported, which
was the existing behavior.

Closes gh-30981
This commit is contained in:
Sam Brannen
2023-08-04 12:10:07 +03:00
parent b80872b762
commit 04cce0bafd
24 changed files with 622 additions and 130 deletions

View File

@@ -18,6 +18,7 @@ package org.springframework.test.context;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
@@ -409,9 +410,9 @@ class MergedContextConfigurationTests {
void equalsWithSameContextCustomizers() {
Set<ContextCustomizer> customizers = Collections.singleton(mock());
MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY,
EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, null, null, customizers, loader, null, null);
EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, List.of(), null, customizers, loader, null, null);
MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY,
EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, null, null, customizers, loader, null, null);
EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, List.of(), null, customizers, loader, null, null);
assertThat(mergedConfig2).isEqualTo(mergedConfig1);
}
@@ -424,9 +425,9 @@ class MergedContextConfigurationTests {
Set<ContextCustomizer> customizers2 = Collections.singleton(mock());
MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY,
EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, null, null, customizers1, loader, null, null);
EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, List.of(), null, customizers1, loader, null, null);
MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY,
EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, null, null, customizers2, loader, null, null);
EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, List.of(), null, customizers2, loader, null, null);
assertThat(mergedConfig2).isNotEqualTo(mergedConfig1);
assertThat(mergedConfig1).isNotEqualTo(mergedConfig2);
}

View File

@@ -0,0 +1,62 @@
/*
* 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 java.util.Properties;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.env.PropertiesPropertySource;
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.StringUtils;
/**
* Demo {@link PropertySourceFactory} that provides YAML support.
*
* @author Sam Brannen
* @since 6.1
*/
class YamlPropertySourceFactory implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) {
Resource resource = encodedResource.getResource();
if (!StringUtils.hasText(name)) {
name = getNameForResource(resource);
}
YamlPropertiesFactoryBean factoryBean = new YamlPropertiesFactoryBean();
factoryBean.setResources(resource);
factoryBean.afterPropertiesSet();
Properties properties = factoryBean.getObject();
return new PropertiesPropertySource(name, properties);
}
/**
* Return the description for the given Resource; if the description is
* empty, return the class name of the resource plus its identity hash code.
*/
private static String getNameForResource(Resource resource) {
String name = resource.getDescription();
if (!StringUtils.hasText(name)) {
name = resource.getClass().getSimpleName() + "@" + System.identityHashCode(resource);
}
return name;
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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 java.lang.annotation.ElementType;
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;
/**
* @author Sam Brannen
* @since 6.1
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@TestPropertySource(factory = YamlPropertySourceFactory.class)
public @interface YamlTestProperties {
@AliasFor(annotation = TestPropertySource.class)
String[] value();
}

View File

@@ -0,0 +1,59 @@
/*
* 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.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.io.support.PropertySourceFactory;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link TestPropertySource @TestPropertySource} support
* with a custom YAML {@link PropertySourceFactory}.
*
* @author Sam Brannen
* @since 6.1
*/
@SpringJUnitConfig
@YamlTestProperties("test-properties.yaml")
class YamlTestPropertySourceTests {
@ParameterizedTest
@CsvSource(delimiterString = "->", textBlock = """
environments.dev.url -> https://dev.example.com
environments.dev.name -> 'Developer Setup'
environments.prod.url -> https://prod.example.com
environments.prod.name -> 'My Cool App'
""")
void propertyIsAvailableInEnvironment(String property, String value, @Autowired ConfigurableEnvironment env) {
assertThat(env.getProperty(property)).isEqualTo(value);
}
@Configuration
static class Config {
/* no user beans required for these tests */
}
}

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");
* you may not use this file except in compliance with the License.
@@ -16,15 +16,9 @@
package org.springframework.test.context.env.repeatable;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.env.repeatable.LocalPropertiesFileAndMetaPropertiesFileTests.MetaFileTestProperty;
/**
* Integration tests for {@link TestPropertySource @TestPropertySource} as a
@@ -48,15 +42,4 @@ class LocalPropertiesFileAndMetaPropertiesFileTests extends AbstractRepeatableTe
assertEnvironmentValue("key2", "meta file");
}
/**
* Composed annotation that declares a properties file via
* {@link TestPropertySource @TestPropertySource}.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@TestPropertySource("meta.properties")
@interface MetaFileTestProperty {
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.repeatable;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.env.YamlTestProperties;
/**
* Analogous to {@link LocalPropertiesFileAndMetaPropertiesFileTests} except
* that the local file is YAML.
*
* @author Sam Brannen
* @since 6.1
*/
@YamlTestProperties("local.yaml")
@MetaFileTestProperty
class LocalYamlFileAndMetaPropertiesFileTests extends AbstractRepeatableTestPropertySourceTests {
@Test
void test() {
assertEnvironmentValue("key1", "local file");
assertEnvironmentValue("key2", "meta file");
assertEnvironmentValue("environments.dev.url", "https://dev.example.com");
assertEnvironmentValue("environments.dev.name", "Developer Setup");
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.repeatable;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.test.context.TestPropertySource;
/**
* Composed annotation that declares a properties file via
* {@link TestPropertySource @TestPropertySource}.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@TestPropertySource("meta.properties")
@interface MetaFileTestProperty {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* 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.
@@ -187,6 +187,7 @@ class BootstrapTestUtilsMergedConfigTests extends AbstractContextConfigurationUt
assertMergedConfigForLocationPaths(RelativeFooXmlLocation.class);
}
@SuppressWarnings("deprecation")
private void assertMergedConfigForLocationPaths(Class<?> testClass) {
MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass);

View File

@@ -18,7 +18,9 @@ package org.springframework.test.context.support;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
@@ -29,6 +31,7 @@ import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PropertySourceDescriptor;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.mock.env.MockPropertySource;
import org.springframework.test.context.TestPropertySource;
@@ -295,7 +298,9 @@ class TestPropertySourceUtilsTests {
MergedTestPropertySources mergedPropertySources = buildMergedTestPropertySources(testClass);
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(mergedPropertySources).isNotNull();
softly.assertThat(mergedPropertySources.getLocations()).isEqualTo(expectedLocations);
Stream<String> locations = mergedPropertySources.getPropertySourceDescriptors().stream()
.map(PropertySourceDescriptor::locations).flatMap(List::stream);
softly.assertThat(locations).containsExactly(expectedLocations);
softly.assertThat(mergedPropertySources.getProperties()).isEqualTo(expectedProperties);
});
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* 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.
@@ -33,14 +33,15 @@ class AnnotationConfigWebContextLoaderTests {
@Test
void configMustNotContainLocations() throws Exception {
@SuppressWarnings("deprecation")
void configMustNotContainLocations() {
AnnotationConfigWebContextLoader loader = new AnnotationConfigWebContextLoader();
WebMergedContextConfiguration mergedConfig = new WebMergedContextConfiguration(getClass(),
new String[] { "config.xml" }, EMPTY_CLASS_ARRAY, null, EMPTY_STRING_ARRAY, EMPTY_STRING_ARRAY,
EMPTY_STRING_ARRAY, "resource/path", loader, null, null);
assertThatIllegalStateException()
.isThrownBy(() -> loader.loadContext(mergedConfig))
.withMessageContaining("does not support resource locations");
.isThrownBy(() -> loader.loadContext(mergedConfig))
.withMessageContaining("does not support resource locations");
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* 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.
@@ -34,12 +34,13 @@ class GenericXmlWebContextLoaderTests {
@Test
void configMustNotContainAnnotatedClasses() throws Exception {
GenericXmlWebContextLoader loader = new GenericXmlWebContextLoader();
@SuppressWarnings("deprecation")
WebMergedContextConfiguration mergedConfig = new WebMergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY,
new Class<?>[] { getClass() }, null, EMPTY_STRING_ARRAY, EMPTY_STRING_ARRAY, EMPTY_STRING_ARRAY,
"resource/path", loader, null, null);
assertThatIllegalStateException()
.isThrownBy(() -> loader.loadContext(mergedConfig))
.withMessageContaining("does not support annotated classes");
.isThrownBy(() -> loader.loadContext(mergedConfig))
.withMessageContaining("does not support annotated classes");
}
}