Justin Knowles opened SPR-14378 and commented
@TestPropertySource
allows you to set test-suite / class-level properties, but I want test-level properties so I can test drive classes with @ConditionalOnProperty
and @Value(${var:default})
type annotations for correct behavior.
I have considered creating separate test suites for each test, but that seems unnatural. I currently manually manage the context within the test to validate the behavior.
Issue Links:
- #13977 Support @ActiveProfiles
at method level
- #16647 Support @ContextConfiguration
at method level
8 votes, 14 watchers
Comment From: spring-projects-issues
Seth Wingert commented
This would be really useful for me as well. I'm doing a lot of custom @AutoConfigure
and managing different properties between tests is really tricky since @TestPropertySource
only works on the class level.
Comment From: spring-projects-issues
vijay parashar commented
Any timeline for fixing this issue.
Comment From: membersound
Any plans on this? It would be very useful to not having to create a separate class each time an application.properties parameter should change for testing.
Comment From: RonaldFindling
We would love to see this aswell
Comment From: drodionov
It would be great to have this feature!
Comment From: srferron
Yes, it´s needed
Comment From: ben-pearson
Yep, 4 years on, I want this too!
Comment From: aarowman
Any updates on this? Would really make testing simpler on conditional properties...
Comment From: bruno-oliveira
Bump
Comment From: joel-regen
I need this too!!
Comment From: TinaTiel
Also could use this feature!
Comment From: ahoehma
I have a working implementation for this.
Comment From: vinay36999
Most developers resort to ReflectionTestUtils.setField(...) to work around this limitation, if the need happens to be to set just couple of properties.
Comment From: pieterdb4477
This would be a great addition, indeed.
Comment From: rahul-gupta-1525
good to have this in spring, looking forward to this
Comment From: bbortt
@ahoehma
I have a working implementation for this.
would you mind sharing it? 😉
Comment From: bfrggit
Should be very helpful when testing some component with different configurations.
Comment From: EarthCitizen
This would be BEYOND incredible. No more nested static test classes just to have different properties for 1 or 2 tests!!!
Comment From: EarthCitizen
@sbrannen Any chance of getting this on the next minor version bump?
Comment From: sbrannen
@sbrannen Any chance of getting this on the next minor version bump?
The change would be too large for a point release. If we implement this, it would come in 6.x.
Comment From: romerorsp
It Will be great when you guys release that change in 6.x! I'd needed it now, but will figure another way of achieving the same goal.
Comment From: sergey-morenets
It Will be great when you guys release that change in 6.x! I'd needed it now, but will figure another way of achieving the same goal.
Hi @romerorsp
You can easily achieve this functionality if you use Junit 5. So you can just extract all the methods into inner class:
@SpringJUnitConfig(AppConfig.class)
public class ServerTest {
@Nested
@TestPropertySource(properties = "db.port=7000")
public class ServerLoadConfiguration {
Comment From: martinwunderlich-celonis
any updates on this? would be a great feature to have.
Comment From: ahoehma
Let me share what I have here:
import static com.google.common.base.Preconditions.checkNotNull;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Properties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.annotation.DirtiesContext.MethodMode;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
/**
* Note: the whole idea of test-method-specific properties is ONLY working if the consuming bean is initialized AFTER spring was able to add the property source to the context. Any other combination
* of early initialized bean, constructor using properties etc. will not work. Even if you put a {@link RefreshScope} on such beans. The problem is how spring is calling this listener here. Its
* sometimes simply too late. Maybe later if we really need/have refresh-able apps (consuming {@link RefreshScopeRefreshedEvent} etc.) then this can work.
*
* @author Andreas Höhmann
*/
public final class TestPropertiesPerMethodHandler extends AbstractTestExecutionListener {
private static final Logger LOGGER = LoggerFactory.getLogger(TestPropertiesPerMethodHandler.class);
private final Multimap<Method, PropertiesPropertySource> propertySources = ArrayListMultimap.create();
@Target({
ElementType.TYPE,
ElementType.METHOD
})
@Retention(RetentionPolicy.RUNTIME)
@DirtiesContext(methodMode = MethodMode.AFTER_METHOD, classMode = ClassMode.AFTER_CLASS)
public @interface TestProperties {
@Target({
ElementType.TYPE,
ElementType.METHOD
})
@Retention(RetentionPolicy.RUNTIME)
@DirtiesContext(methodMode = MethodMode.AFTER_METHOD, classMode = ClassMode.AFTER_CLASS)
public @interface TestProperty {
String name();
String value();
}
TestProperty[] value();
}
private static List<PropertiesPropertySource> getTestProperties(final Method method) {
checkNotNull(method, "Method must not be null");
final List<PropertiesPropertySource> result = Lists.newArrayList();
{
final TestProperties[] allTestEnvironmentProperties = method.getAnnotationsByType(TestProperties.class);
if (allTestEnvironmentProperties != null) {
int c = 0;
for (final TestProperties testEnvironmentProperties : allTestEnvironmentProperties) {
final Properties properties = new Properties();
for (final TestProperty testEnvironmentProperty : testEnvironmentProperties.value()) {
properties.put(testEnvironmentProperty.name(), testEnvironmentProperty.value());
}
if (!properties.isEmpty()) {
result.add(new PropertiesPropertySource(method.getName().toLowerCase() + "-" + c, properties));
c++;
}
}
}
}
{
final TestProperty[] allTestEnvironmentProperties = method.getAnnotationsByType(TestProperty.class);
if (allTestEnvironmentProperties != null) {
final Properties properties = new Properties();
for (final TestProperty testEnvironmentProperty : allTestEnvironmentProperties) {
properties.put(testEnvironmentProperty.name(), testEnvironmentProperty.value());
}
if (!properties.isEmpty()) {
result.add(new PropertiesPropertySource(method.getName().toLowerCase(), properties));
}
}
}
return result;
}
@Override
public void beforeTestMethod(final TestContext testContext) throws Exception {
final Method testMethod = testContext.getTestMethod();
boolean needRefresh = false;
for (final PropertiesPropertySource propertiesPropertySource : getTestProperties(testMethod)) {
LOGGER.info("Register additional property source '{}' with '{}' for method '{}'", propertiesPropertySource.getName(),
propertiesPropertySource.getSource(), testMethod);
((ConfigurableEnvironment) testContext.getApplicationContext().getEnvironment())
.getPropertySources()
.addFirst(propertiesPropertySource);
propertySources.put(testMethod, propertiesPropertySource);
needRefresh = true;
}
if (needRefresh) {
testContext.getApplicationContext().getBean(ContextRefresher.class).refresh();
}
}
@Override
public void afterTestMethod(final TestContext testContext) throws Exception {
final Method testMethod = testContext.getTestMethod();
boolean needRefresh = false;
for (final PropertiesPropertySource propertiesPropertySource : propertySources.removeAll(testMethod)) {
LOGGER.info("Unregister additional property source '{}' with '{}' for method '{}'", propertiesPropertySource.getName(),
propertiesPropertySource.getSource(), testMethod);
((ConfigurableEnvironment) testContext.getApplicationContext().getEnvironment())
.getPropertySources()
.remove(propertiesPropertySource.getName());
needRefresh = true;
if (needRefresh) {
testContext.getApplicationContext().getBean(ContextRefresher.class).refresh();
}
}
}
}
Example:
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.junit.jupiter.api.Test;
import TestPropertiesPerMethodHandler.TestProperties;
import TestPropertiesPerMethodHandler.TestProperties.TestProperty;
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@TestPropertySource(
properties = {
"foo=false",
"logging.level.TestPropertiesPerMethodHandler=DEBUG",
})
@TestExecutionListeners(
listeners = {
TestPropertiesPerMethodHandler.class,
}, mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
class MyTest {
@Test
@TestProperties({
@TestProperty(name = "foo", value = "true"), // will override global "foo"
@TestProperty(name = "bar", value = "false")
})
void test() {
... testing
}
}