Lesiak
Lesiak

Reputation: 26066

Read a Map with Spring @ConfigurationProperties in test

Following a advice from Spring Boot integration tests doesn't read properties files I created the following code, with the intention of reading a map from properties in my JUnit test. (I am using yml format, and using @ConfigurationProperties instead of @Value)

@RunWith(SpringJUnit4ClassRunner.class)
@TestPropertySource(locations="classpath:application-test.yml")
@ContextConfiguration(classes = {PropertiesTest.ConfigurationClass.class, PropertiesTest.ClassToTest.class})
public class PropertiesTest {

    @Configuration
    @EnableConfigurationProperties
    static class ConfigurationClass {
    }


    @ConfigurationProperties
    static class ClassToTest {
        private String test;

        private Map<String, Object> myMap = new HashMap<>();

        public String getTest() {
            return test;
        }

        public void setTest(String test) {
            this.test = test;
        }

        public Map<String, Object> getMyMap() {
            return myMap;
        }

    }

    @Autowired
    private ClassToTest config;


    @Test
    public void testStringConfig() {
        Assert.assertEquals(config.test, "works!");
    }

    @Test
    public void testMapConfig() {
        Assert.assertEquals(config.myMap.size(), 1);
    }

}

My test configuration (in application-test.yml):

test: works!
myMap:
  aKey: aVal
  aKey2: aVal2

Strangely, the String "works!" is successfully read from the config file, but the map is not populated.

What am I missing?

Note: adding a map setter causes the following exception:

Caused by: org.springframework.validation.BindException: org.springframework.boot.bind.RelaxedDataBinder$RelaxedBeanPropertyBindingResult: 1 errors
Field error in object 'target' on field 'myMap': rejected value []; codes [typeMismatch.target.myMap,typeMismatch.myMap,typeMismatch.java.util.Map,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [target.myMap,myMap]; arguments []; default message [myMap]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.util.Map' for property 'myMap'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'java.util.Map' for property 'myMap': no matching editors or conversion strategy found]
    at org.springframework.boot.bind.PropertiesConfigurationFactory.checkForBindingErrors(PropertiesConfigurationFactory.java:359)
    at org.springframework.boot.bind.PropertiesConfigurationFactory.doBindPropertiesToTarget(PropertiesConfigurationFactory.java:276)
    at org.springframework.boot.bind.PropertiesConfigurationFactory.bindPropertiesToTarget(PropertiesConfigurationFactory.java:240)
    at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:330)
    ... 42 more

Upvotes: 4

Views: 3788

Answers (1)

Lesiak
Lesiak

Reputation: 26066

After some wonderful time with a debugger, I believe that this is a bug / missing feature in TestPropertySourceUtils.addPropertiesFilesToEnvironment():

try {
    for (String location : locations) {
        String resolvedLocation = environment.resolveRequiredPlaceholders(location);
        Resource resource = resourceLoader.getResource(resolvedLocation);
        environment.getPropertySources().addFirst(new ResourcePropertySource(resource));
    }
}

ResourcePropertySource can only deal with .properties files and not .yml. In regular app, YamlPropertySourceLoader registered and can deal with .yml.

As a note: TestPropertySourceUtils.addPropertiesFilesToEnvironment() is called by:

org.springframework.test.context.support.DelegatingSmartContextLoader.prepareContext()

(inherited from AbstractContextLoader)

DelegatingSmartContextLoader is the default context loader you receive if no loader is specified in @ContextConfiguration. (in fact @ContextConfiguration specifies an interface, but AbstractTestContextBootstrapper.resolveContextLoader() changes it to a concrete class)

To resolve the problem, I changed my configuration to application-test.properties and used that file in my test.

test=works!
myMap.aKey: aVal

Another comment: the setter on the map is NOT needed:

https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-loading-yaml

To bind to properties like that using the Spring DataBinder utilities (which is what @ConfigurationProperties does) you need to have a property in the target bean of type java.util.List (or Set) and you either need to provide a setter, or initialize it with a mutable value, e.g. this will bind to the properties above

Upvotes: 1

Related Questions