Add @DynamicPropertySource support in TestContext framework

This commit introduces a @DynamicPropertySource annotation that can be
used on methods in test classes that want to add properties to the
Environment with a dynamically supplied value.

This new feature can be used in conjunction with Testcontainers and
other frameworks that manage resources outside the lifecycle of a
test's ApplicationContext.

Closes gh-24540

Co-authored-by: Phillip Webb <pwebb@pivotal.io>
This commit is contained in:
Sam Brannen
2020-03-17 14:10:30 -07:00
parent 821a8eebdd
commit cf7daa36c8
11 changed files with 781 additions and 2 deletions

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2002-2020 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;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration test for {@link DynamicPropertySource @DynamicPropertySource}.
*
* @author Phillip Webb
* @author Sam Brannen
*/
@SpringJUnitConfig
class DynamicPropertySourceIntegrationTests {
static DemoContainer container = new DemoContainer();
@DynamicPropertySource
static void containerProperties(DynamicPropertyRegistry registry) {
registry.add("test.container.ip", container::getIpAddress);
registry.add("test.container.port", container::getPort);
}
@Test
void hasInjectedValues(@Autowired Service service) {
assertThat(service.getIp()).isEqualTo("127.0.0.1");
assertThat(service.getPort()).isEqualTo(4242);
}
@Configuration
@Import(Service.class)
static class Config {
}
@Component
static class Service {
private final String ip;
private final int port;
Service(@Value("${test.container.ip}") String ip, @Value("${test.container.port}") int port) {
this.ip = ip;
this.port = port;
}
String getIp() {
return this.ip;
}
int getPort() {
return this.port;
}
}
static class DemoContainer {
String getIpAddress() {
return "127.0.0.1";
}
int getPort() {
return 4242;
}
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2002-2020 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.support;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DynamicPropertiesContextCustomizerFactory}.
*
* @author Phillip Webb
*/
class DynamicPropertiesContextCustomizerFactoryTests {
private final DynamicPropertiesContextCustomizerFactory factory = new DynamicPropertiesContextCustomizerFactory();
private final List<ContextConfigurationAttributes> configAttributes = Collections.emptyList();
@Test
void createContextCustomizerWhenNoAnnotatedMethodsReturnsNull() {
DynamicPropertiesContextCustomizer customizer = this.factory.createContextCustomizer(
NoDynamicPropertySource.class, this.configAttributes);
assertThat(customizer).isNull();
}
@Test
void createContextCustomizerWhenSingleAnnotatedMethodReturnsCustomizer() {
DynamicPropertiesContextCustomizer customizer = this.factory.createContextCustomizer(
SingleDynamicPropertySource.class, this.configAttributes);
assertThat(customizer).isNotNull();
assertThat(customizer.getMethods()).flatExtracting(Method::getName).containsOnly("p1");
}
@Test
void createContextCustomizerWhenMultipleAnnotatedMethodsReturnsCustomizer() {
DynamicPropertiesContextCustomizer customizer = this.factory.createContextCustomizer(
MultipleDynamicPropertySources.class, this.configAttributes);
assertThat(customizer).isNotNull();
assertThat(customizer.getMethods()).flatExtracting(Method::getName).containsOnly("p1", "p2", "p3");
}
@Test
void createContextCustomizerWhenAnnotatedMethodsInBaseClassReturnsCustomizer() {
DynamicPropertiesContextCustomizer customizer = this.factory.createContextCustomizer(
SubDynamicPropertySource.class, this.configAttributes);
assertThat(customizer).isNotNull();
assertThat(customizer.getMethods()).flatExtracting(Method::getName).containsOnly("p1", "p2");
}
static class NoDynamicPropertySource {
void empty() {
}
}
static class SingleDynamicPropertySource {
@DynamicPropertySource
static void p1(DynamicPropertyRegistry registry) {
}
}
static class MultipleDynamicPropertySources {
@DynamicPropertySource
static void p1(DynamicPropertyRegistry registry) {
}
@DynamicPropertySource
static void p2(DynamicPropertyRegistry registry) {
}
@DynamicPropertySource
static void p3(DynamicPropertyRegistry registry) {
}
}
static class BaseDynamicPropertySource {
@DynamicPropertySource
static void p1(DynamicPropertyRegistry registry) {
}
}
static class SubDynamicPropertySource extends BaseDynamicPropertySource {
@DynamicPropertySource
static void p2(DynamicPropertyRegistry registry) {
}
}
}

View File

@@ -0,0 +1,153 @@
/*
* Copyright 2002-2020 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.support;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DynamicPropertiesContextCustomizer}.
*
* @author Phillip Webb
* @author Sam Brannen
*/
class DynamicPropertiesContextCustomizerTests {
@Test
void createWhenNonStaticDynamicPropertiesMethodThrowsException() {
assertThatIllegalStateException()
.isThrownBy(() -> customizerFor("nonStatic"))
.withMessage("@DynamicPropertySource method 'nonStatic' must be static");
}
@Test
void createWhenBadDynamicPropertiesSignatureThrowsException() {
assertThatIllegalStateException()
.isThrownBy(() -> customizerFor("badArgs"))
.withMessage("@DynamicPropertySource method 'badArgs' must accept a single DynamicPropertyRegistry argument");
}
@Test
void nullPropertyNameResultsInException() throws Exception {
DynamicPropertiesContextCustomizer customizer = customizerFor("nullName");
ConfigurableApplicationContext context = new StaticApplicationContext();
assertThatIllegalArgumentException()
.isThrownBy(() -> customizer.customizeContext(context, mock(MergedContextConfiguration.class)))
.withMessage("'name' must not be null or blank");
}
@Test
void emptyPropertyNameResultsInException() throws Exception {
DynamicPropertiesContextCustomizer customizer = customizerFor("emptyName");
ConfigurableApplicationContext context = new StaticApplicationContext();
assertThatIllegalArgumentException()
.isThrownBy(() -> customizer.customizeContext(context, mock(MergedContextConfiguration.class)))
.withMessage("'name' must not be null or blank");
}
@Test
void nullValueSupplierResultsInException() throws Exception {
DynamicPropertiesContextCustomizer customizer = customizerFor("nullValueSupplier");
ConfigurableApplicationContext context = new StaticApplicationContext();
assertThatIllegalArgumentException()
.isThrownBy(() -> customizer.customizeContext(context, mock(MergedContextConfiguration.class)))
.withMessage("'valueSupplier' must not be null");
}
@Test
void customizeContextAddsPropertySource() throws Exception {
ConfigurableApplicationContext context = new StaticApplicationContext();
DynamicPropertiesContextCustomizer customizer = customizerFor("valid1", "valid2");
customizer.customizeContext(context, mock(MergedContextConfiguration.class));
ConfigurableEnvironment environment = context.getEnvironment();
assertThat(environment.getRequiredProperty("p1a")).isEqualTo("v1a");
assertThat(environment.getRequiredProperty("p1b")).isEqualTo("v1b");
assertThat(environment.getRequiredProperty("p2a")).isEqualTo("v2a");
assertThat(environment.getRequiredProperty("p2b")).isEqualTo("v2b");
}
@Test
void equalsAndHashCode() {
DynamicPropertiesContextCustomizer c1 = customizerFor("valid1", "valid2");
DynamicPropertiesContextCustomizer c2 = customizerFor("valid1", "valid2");
DynamicPropertiesContextCustomizer c3 = customizerFor("valid1");
assertThat(c1.hashCode()).isEqualTo(c1.hashCode()).isEqualTo(c2.hashCode());
assertThat(c1).isEqualTo(c1).isEqualTo(c2).isNotEqualTo(c3);
}
private static DynamicPropertiesContextCustomizer customizerFor(String...methods) {
return new DynamicPropertiesContextCustomizer(findMethods(methods));
}
private static Set<Method> findMethods(String... names) {
Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(DynamicPropertySourceTestCase.class,
method -> ObjectUtils.containsElement(names, method.getName()));
return new LinkedHashSet<>(Arrays.asList(methods));
}
static class DynamicPropertySourceTestCase {
void nonStatic(DynamicPropertyRegistry registry) {
}
static void badArgs(String bad) {
}
static void nullName(DynamicPropertyRegistry registry) {
registry.add(null, () -> "A");
}
static void emptyName(DynamicPropertyRegistry registry) {
registry.add(" ", () -> "A");
}
static void nullValueSupplier(DynamicPropertyRegistry registry) {
registry.add("name", null);
}
static void valid1(DynamicPropertyRegistry registry) {
registry.add("p1a", () -> "v1a");
registry.add("p1b", () -> "v1b");
}
static void valid2(DynamicPropertyRegistry registry) {
registry.add("p2a", () -> "v2a");
registry.add("p2b", () -> "v2b");
}
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2002-2020 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.support;
import java.util.HashMap;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link DynamicValuesPropertySource}.
*
* @author Phillip Webb
* @author Sam Brannen
*/
class DynamicValuesPropertySourceTests {
@SuppressWarnings("serial")
private final DynamicValuesPropertySource source = new DynamicValuesPropertySource("test",
new HashMap<String, Supplier<Object>>() {{
put("a", () -> "A");
put("b", () -> "B");
}});
@Test
void getPropertyReturnsSuppliedProperty() throws Exception {
assertThat(this.source.getProperty("a")).isEqualTo("A");
assertThat(this.source.getProperty("b")).isEqualTo("B");
}
@Test
void getPropertyWhenMissingReturnsNull() throws Exception {
assertThat(this.source.getProperty("c")).isNull();
}
@Test
void containsPropertyWhenPresentReturnsTrue() {
assertThat(this.source.containsProperty("a")).isTrue();
assertThat(this.source.containsProperty("b")).isTrue();
}
@Test
void containsPropertyWhenMissingReturnsFalse() {
assertThat(this.source.containsProperty("c")).isFalse();
}
@Test
void getPropertyNamesReturnsNames() {
assertThat(this.source.getPropertyNames()).containsExactly("a", "b");
}
}