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
  }
}