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:
The configuration
Our configuration wasn’t very complicated – it was just large. Below you can see a simplified example with two properties. In reality many more followed. Some properties are mandatory while others are optional. Additionally some properties have default values which can be overwritten.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@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 testing with the happy path. The test was pretty simple and worked well. You can see it below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
@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 } } 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@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); assertThat(properties.getSomeDefaultProperty()).isEqualTo("default value"); } @EnableConfigurationProperties(MyConfigurationProperties.class) public static class TestConfiguration { // nothing } } 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…
Programmatically loading @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 quite simple manner in a single test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
@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.<MyConfigurationProperties>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.<MyConfigurationProperties>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.<MyConfigurationProperties>builder() .populate(new MyConfigurationProperties()) .fromFile("application.yml") .withPrefix("my.properties") .validateUsing(localValidatorFactoryBean) .withoutProperty("my.properties.some_mandatory_property") .build(); } } 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
public class ConfigurationPropertiesBuilder<T> { private T object; private String fileName; private String prefix; private Validator validator; private Properties properties = new Properties(); private List<String> propertiesToRemove = new ArrayList<>(); public static <T> ConfigurationPropertiesBuilder<T> builder() { return new ConfigurationPropertiesBuilder<T>(); } public ConfigurationPropertiesBuilder<T> populate(T object) { this.object = object; return this; } public ConfigurationPropertiesBuilder<T> fromFile(String fileName) { this.fileName = fileName; return this; } public ConfigurationPropertiesBuilder<T> withPrefix(String prefix) { this.prefix = prefix; return this; } public ConfigurationPropertiesBuilder<T> validateUsing(Validator validator) { this.validator = validator; return this; } public ConfigurationPropertiesBuilder<T> withProperty(String key, String value) { properties.setProperty(key, value); return this; } public ConfigurationPropertiesBuilder<T> 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<T> 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.
1 2 3 4 |
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
With Spring Boot 2 PropertiesConfigurationFactory not exists any more. Use the new Binder instead:
Binder binder = new Binder(propertySources);
binder.bind(prefix, MyConfigurationProperties.class).get();
Downloaded your example from github
can’t find PropertiesConfigurationFactory….to understand the example
You helped me a lot. So thanks!
An example how to load properties with Spring Boot 2 (in groovy, so for java you need to replace def with correct types):
private MyConfigurationProperties loadYamlProperties(String fileName) {
def propertiesFactoryBean = new YamlPropertiesFactoryBean()
propertiesFactoryBean.setResources(new ClassPathResource(fileName))
def properties = propertiesFactoryBean.getObject()
def propertySource = ConfigurationPropertySources.from(new PropertiesPropertySource(‘properties’, properties))
def binder = new Binder(propertySource)
binder.bind(prefix, Bindable.of(MyConfigurationProperties), new ValidationBindHandler(localValidatorFactoryBean)).get()
}