Testing @ConfigurationProperties in Spring Boot

Thomas Uhrig · May 30, 2018

Update: I updated the example on GitHub to Spring 2.5.X! The latest post on this topic is here.

I recently worked on a library for using AWS Kinesis in Spring Boot. As many other libraries, this particular one provided a powerful configuration. To implement the configuration, we used Spring Boot’s @ConfigurationProperties (as described here). This article gives some insights on how we did our testing.

You can find the source code on GitHub:

https://github.com/tuhrig/Testing_Configuration_Properties

The configuration

Our configuration wasn’t very complicated - it was just large. Below you can see a simplified example. Some properties are mandatory while others are optional. Additionally, some properties have default values which can be overwritten.

@Data
@ConfigurationProperties(prefix = "my.properties")
public class MyConfigurationProperties {

    @NotNull
    private String someMandatoryProperty;

    private String someOptionalProperty;

    private String someDefaultProperty = "default value";

    // many more...
}

(View on GitHub)

The happy path

We started by testing the happy path. The test was pretty simple and worked well. You can see it below.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { MyConfigurationPropertiesTest_1.TestConfiguration.class })
@ActiveProfiles("happy-path")
public class MyConfigurationPropertiesTest_1 {

    @Autowired
    private MyConfigurationProperties properties;

    @Test
    public void should_Populate_MyConfigurationProperties() {
        assertThat(properties.getSomeMandatoryProperty()).isEqualTo("123456");
        assertThat(properties.getSomeOptionalProperty()).isEqualTo("abcdef");
        assertThat(properties.getSomeDefaultProperty()).isEqualTo("overwritten");
    }

    @EnableConfigurationProperties(MyConfigurationProperties.class)
    public static class TestConfiguration {
        // nothing
    }
}

With application-happy-path.yml:

my:
  properties:
    some_mandatory_property: "123456"
    some_optional_property: "abcdef"
    some_default_property: "overwritten"

(View on GitHub)

Different variations

Our next step was to test different variations of the configuration. Since some properties are optional, we wanted to test what will happen if they are missing. So we wrote another test:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { MyConfigurationPropertiesTest_2.TestConfiguration.class })
@ActiveProfiles("minimal")
public class MyConfigurationPropertiesTest_2 {

    @Autowired
    private MyConfigurationProperties properties;

    @Test
    public void should_Populate_MyConfigurationProperties() {
        assertThat(properties.getSomeMandatoryProperty()).isEqualTo("123456");
        assertThat(properties.getSomeOptionalProperty()).isEqualTo(null); /* null! */
        assertThat(properties.getSomeDefaultProperty()).isEqualTo("default value");
    }

    @EnableConfigurationProperties(MyConfigurationProperties.class)
    public static class TestConfiguration {
        // nothing
    }
}

With application-minimal.yml:

    my:
      properties:
        some_mandatory_property: "123456"

(View on GitHub)

The problem

However, there was a problem with this approach: we needed to write a complete test class for each scenario:

  • MyConfigurationPropertiesTest_1.java
  • MyConfigurationPropertiesTest_2.java
  • …and so on…

Each variation would require us to create a new class and a new application-xxx.yml file. That wasn’t to end well.

Builder for @ConfigurationProperties

Our solution was to load the @ConfigurationProperties programmatically instead of just wiring them with Spring. This gave us the possibility to modify them in each test. To make life even easier we wrote a ConfigurationPropertiesBuilder for our test cases. You can see the result below. We accomplished to test a lot of cases in a single test:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { ValidationAutoConfiguration.class })
public class MyConfigurationPropertiesTest {
 
    @Autowired
    private LocalValidatorFactoryBean localValidatorFactoryBean;
 
    @Test
    public void should_Populate_MyConfigurationProperties() throws Exception {
 
        MyConfigurationProperties properties = ConfigurationPropertiesBuilder
                 .builder()
                 .populate(new MyConfigurationProperties())
                 .fromFile("application.yml")
                 .withPrefix("my.properties")
                 .validateUsing(localValidatorFactoryBean)
                 .withProperty("my.properties.some-optional-property", "abcdef")
                 .withProperty("my.properties.some-default-property", "overwritten")
                 .build();
 
        assertThat(properties.getSomeMandatoryProperty()).isEqualTo("123456");
        assertThat(properties.getSomeOptionalProperty()).isEqualTo("abcdef");
        assertThat(properties.getSomeDefaultProperty()).isEqualTo("overwritten");
    }
 
    @Test
    public void should_Populate_MyConfigurationProperties\_WithMandatoryPropertiesOnly() throws Exception {
 
        MyConfigurationProperties properties = ConfigurationPropertiesBuilder
                 .builder()
                 .populate(new MyConfigurationProperties())
                 .fromFile("application.yml")
                 .withPrefix("my.properties")
                 .validateUsing(localValidatorFactoryBean)
                 .build();
 
        assertThat(properties.getSomeMandatoryProperty()).isEqualTo("123456");
        assertThat(properties.getSomeOptionalProperty()).isEqualTo(null);
        assertThat(properties.getSomeDefaultProperty()).isEqualTo("default value");
    }
 
    @Test(expected = BindException.class)
    public void should_ThrowException_IfMandatoryPropertyIsMissing() throws Exception {
        ConfigurationPropertiesBuilder
                .builder()
                .populate(new MyConfigurationProperties())
                .fromFile("application.yml")
                .withPrefix("my.properties")
                .validateUsing(localValidatorFactoryBean)
                .withoutProperty("my.properties.some_mandatory_property")
                .build();
    }
}

With application.yml:

    my:
      properties:
        some_mandatory_property: "123456" 

(View on GitHub)

The builder we used to set up our properties looks like below. It provides different options in order to build the properties. For example, you can set properties from a YAML file or programmatically.

public class ConfigurationPropertiesBuilder {

    private T object;
    private String fileName;
    private String prefix;
    private Validator validator;
    private Properties properties = new Properties();
    private List propertiesToRemove = new ArrayList<>();

    public static ConfigurationPropertiesBuilder builder() {
        return new ConfigurationPropertiesBuilder();
    }

    public ConfigurationPropertiesBuilder populate(T object) {
        this.object = object;
        return this;
    }

    public ConfigurationPropertiesBuilder fromFile(String fileName) {
        this.fileName = fileName;
        return this;
    }

    public ConfigurationPropertiesBuilder withPrefix(String prefix) {
        this.prefix = prefix;
        return this;
    }

    public ConfigurationPropertiesBuilder validateUsing(Validator validator) {
        this.validator = validator;
        return this;
    }

    public ConfigurationPropertiesBuilder withProperty(String key, String value) {
        properties.setProperty(key, value);
        return this;
    }

    public ConfigurationPropertiesBuilder withoutProperty(String key) {
        propertiesToRemove.add(key);
        return this;
    }

    public T build() throws BindException {

        Properties propertiesFromFile = loadYamlProperties(fileName);
        propertiesToRemove.forEach(properties::remove);
        propertiesToRemove.forEach(propertiesFromFile::remove);

        MutablePropertySources propertySources = new MutablePropertySources();
        propertySources.addLast(new PropertiesPropertySource("properties", properties));
        propertySources.addLast(new PropertiesPropertySource("propertiesFromFile", propertiesFromFile));

        PropertiesConfigurationFactory configurationFactory = new PropertiesConfigurationFactory<>(object);
        configurationFactory.setPropertySources(propertySources);
        configurationFactory.setTargetName(prefix);
        configurationFactory.setValidator(validator);
        configurationFactory.bindPropertiesToTarget();

        return object;
    }

    private Properties loadYamlProperties(String fileName) {
        Resource resource = new ClassPathResource(fileName);
        YamlPropertiesFactoryBean factoryBean = new YamlPropertiesFactoryBean();
        factoryBean.setResources(resource);
        return factoryBean.getObject();
    }
} 

(View on GitHub)

Notes

In order to test the validation, we needed to add some dependencies to our project. Depending on your setup, this might be needed or not.

    compile "javax.validation:validation-api:2.0.0.Final"

    testCompile "org.hibernate:hibernate-validator:6.0.8.Final"
    testCompile "org.glassfish:javax.el:3.0.0"

Example on GitHub

I have published this example on GitHub:

https://github.com/tuhrig/Testing_Configuration_Properties

Best regards, Thomas.