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,40 @@
/*
* 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 java.util.function.Supplier;
/**
* Registry used with {@link DynamicPropertySource @DynamicPropertySource}
* methods so that they can add properties to the {@code Environment} that have
* dynamically resolved values.
*
* @author Phillip Webb
* @author Sam Brannen
* @since 5.2.5
* @see DynamicPropertySource
*/
public interface DynamicPropertyRegistry {
/**
* Add a {@link Supplier} for the given property name to this registry.
* @param name the name of the property for which the supplier should be added
* @param valueSupplier a supplier that will provide the property value on demand
*/
void add(String name, Supplier<Object> valueSupplier);
}

View File

@@ -0,0 +1,75 @@
/*
* 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 java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Method-level annotation for integration tests that need to add properties with
* dynamic values to the {@code Environment}'s set of {@code PropertySources}.
*
* <p>This annotation and its supporting infrastructure were originally designed
* to allow properties from
* <a href="https://www.testcontainers.org/">Testcontainers</a> based tests to be
* exposed easily to Spring integration tests. However, this feature may also be
* used with any form of external resource whose lifecycle is maintained outside
* the test's {@code ApplicationContext}.
*
* <p>Methods annotated with {@code @DynamicPropertySource} must be {@code static}
* and must have a single {@link DynamicPropertyRegistry} argument which is used
* to add <em>name-value</em> pairs to the {@code Environment}'s set of
* {@code PropertySources}. Values are dynamic and provided via a {@link Supplier}
* which is only invoked when the property is resolved. Typically, method references
* are used to supply values, as in the following example.
*
* <h3>Example</h3>
*
* <pre class="code">
* &#064;SpringJUnitConfig(...)
* &#064;Testcontainers
* class ExampleIntegrationTests {
*
* &#064;Container
* static RedisContainer redis = new RedisContainer();
*
* // ...
*
* &#064;DynamicPropertySource
* static void redisProperties(DynamicPropertyRegistry registry) {
* registry.add("redis.host", redis::getContainerIpAddress);
* registry.add("redis.port", redis::getMappedPort);
* }
*
* }</pre>
*
* @author Phillip Webb
* @author Sam Brannen
* @since 5.2.5
* @see DynamicPropertyRegistry
* @see ContextConfiguration
* @see TestPropertySource
* @see org.springframework.core.env.PropertySource
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicPropertySource {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* 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.
@@ -81,6 +81,7 @@ import org.springframework.core.annotation.AliasFor;
* @author Sam Brannen
* @since 4.1
* @see ContextConfiguration
* @see DynamicPropertySource
* @see org.springframework.core.env.Environment
* @see org.springframework.core.env.PropertySource
* @see org.springframework.context.annotation.PropertySource

View File

@@ -0,0 +1,111 @@
/*
* 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.lang.reflect.Modifier;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.lang.Nullable;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* {@link ContextCustomizer} to support
* {@link DynamicPropertySource @DynamicPropertySource} methods.
*
* @author Phillip Webb
* @author Sam Brannen
* @since 5.2.5
* @see DynamicPropertiesContextCustomizerFactory
*/
class DynamicPropertiesContextCustomizer implements ContextCustomizer {
private static final String PROPERTY_SOURCE_NAME = "Dynamic Test Properties";
private final Set<Method> methods;
DynamicPropertiesContextCustomizer(Set<Method> methods) {
methods.forEach(this::assertValid);
this.methods = methods;
}
private void assertValid(Method method) {
Assert.state(Modifier.isStatic(method.getModifiers()),
() -> "@DynamicPropertySource method '" + method.getName() + "' must be static");
Class<?>[] types = method.getParameterTypes();
Assert.state(types.length == 1 && types[0] == DynamicPropertyRegistry.class,
() -> "@DynamicPropertySource method '" + method.getName() + "' must accept a single DynamicPropertyRegistry argument");
}
@Override
public void customizeContext(ConfigurableApplicationContext context,
MergedContextConfiguration mergedConfig) {
MutablePropertySources sources = context.getEnvironment().getPropertySources();
sources.addFirst(new DynamicValuesPropertySource(PROPERTY_SOURCE_NAME, buildDynamicPropertiesMap()));
}
@Nullable
private Map<String, Supplier<Object>> buildDynamicPropertiesMap() {
Map<String, Supplier<Object>> map = new LinkedHashMap<>();
DynamicPropertyRegistry dynamicPropertyRegistry = (name, valueSupplier) -> {
Assert.hasText(name, "'name' must not be null or blank");
Assert.notNull(valueSupplier, "'valueSupplier' must not be null");
map.put(name, valueSupplier);
};
this.methods.forEach(method -> {
ReflectionUtils.makeAccessible(method);
ReflectionUtils.invokeMethod(method, null, dynamicPropertyRegistry);
});
return Collections.unmodifiableMap(map);
}
Set<Method> getMethods() {
return this.methods;
}
@Override
public int hashCode() {
return this.methods.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
return this.methods.equals(((DynamicPropertiesContextCustomizer) obj).methods);
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.List;
import java.util.Set;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.lang.Nullable;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizerFactory;
import org.springframework.test.context.DynamicPropertySource;
/**
* {@link ContextCustomizerFactory} to support
* {@link DynamicPropertySource @DynamicPropertySource} methods.
*
* @author Phillip Webb
* @since 5.2.5
* @see DynamicPropertiesContextCustomizer
*/
class DynamicPropertiesContextCustomizerFactory implements ContextCustomizerFactory {
@Override
@Nullable
public DynamicPropertiesContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
Set<Method> methods = MethodIntrospector.selectMethods(testClass, this::isAnnotated);
if (methods.isEmpty()) {
return null;
}
return new DynamicPropertiesContextCustomizer(methods);
}
private boolean isAnnotated(Method method) {
return MergedAnnotations.from(method).isPresent(DynamicPropertySource.class);
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.Map;
import java.util.function.Supplier;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.util.StringUtils;
/**
* {@link EnumerablePropertySource} backed by a map with dynamically supplied
* values.
*
* @author Phillip Webb
* @author Sam Brannen
* @since 5.2.5
*/
class DynamicValuesPropertySource extends EnumerablePropertySource<Map<String, Supplier<Object>>> {
DynamicValuesPropertySource(String name, Map<String, Supplier<Object>> valueSuppliers) {
super(name, valueSuppliers);
}
@Override
public Object getProperty(String name) {
Supplier<Object> valueSupplier = this.source.get(name);
return (valueSupplier != null ? valueSupplier.get() : null);
}
@Override
public boolean containsProperty(String name) {
return this.source.containsKey(name);
}
@Override
public String[] getPropertyNames() {
return StringUtils.toStringArray(this.source.keySet());
}
}