Generic Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

In our previous guide to @ConfigurationProperties, we learned how to set up and use the @ConfigurationProperties annotation with Spring Boot for working with external configuration.

In this tutorial, we'll show how to test configuration classes that rely on the @ConfigurationProperties annotation to make sure that our configuration data is loaded and bound correctly to their corresponding fields.

2. Dependencies

In our Maven project, we'll use the spring-boot-starter and spring-boot-starter-test dependencies to enable the core spring API and Spring's test API, respectively:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.2.RELEASE</version>
</parent>
	
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

Also, let's configure our project with bean validation dependencies since we'll use them later:

<!-- JSR-380 bean validation -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
    <groupId>javax.el</groupId>
    <artifactId>javax.el-api</artifactId>
    <version>3.0.0</version>
</dependency>
<dependency>
    <groupId>org.glassfish.web</groupId>
    <artifactId>javax.el</artifactId>
    <version>2.2.6</version>
</dependency>

3. Properties Binding to User Defined POJOs

When working with externalized configuration, we typically create POJOs containing fields that correspond with the matching configuration properties. As we already know, Spring will then automatically bind the configuration properties to the Java classes we create.

To start with, let's assume that we've got some server configuration inside a properties file we'll call src/test/resources/server-config-test.properties:

server.address.ip=192.168.0.1
server.resources_path.imgs=/root/imgs

Now, let's define a simple configuration class corresponding to the previous properties file:

@Configuration
@ConfigurationProperties(prefix = "server")
public class ServerConfig {

    private Address address;
    private Map<String, String> resourcesPath;

    // getters and setters
}

and also the corresponding Address type:

public class Address {

    private String ip;

    // getters and setters
}

Finally, let's inject the ServerConfig POJO into our test class and validate that all of its fields are set correctly:

@ExtendWith(SpringExtension.class)
@EnableConfigurationProperties(value = ServerConfig.class)
@TestPropertySource("classpath:server-config-test.properties")
public class BindingPropertiesToUserDefinedPOJOUnitTest {

    @Autowired
    private ServerConfig serverConfig;

    @Test
    void givenUserDefinedPOJO_whenBindingPropertiesFile_thenAllFieldsAreSet() {
        assertEquals("192.168.0.1", serverConfig.getAddress().getIp());

        Map<String, String> expectedResourcesPath = new HashMap<>();
        expectedResourcesPath.put("imgs", "/root/imgs");
        assertEquals(expectedResourcesPath, serverConfig.getResourcesPath());
    }
}

In this test, we've used the following annotations:

4. @ConfigurationProperties on @Bean Methods

Another way of creating configuration beans is by using the @ConfigurationProperties annotation on @Bean methods.

For example, the following getDefaultConfigs() method creates a ServerConfig configuration bean:

@Configuration
public class ServerConfigFactory {

    @Bean(name = "default_bean")
    @ConfigurationProperties(prefix = "server.default")
    public ServerConfig getDefaultConfigs() {
        return new ServerConfig();
    }
}

As we can see, we're able to configure the ServerConfig instance using @ConfigurationProperties on the getDefaultConfigs() method, without having to edit the ServerConfig class itself. This can be particularly helpful when working with an external third-party class that has restricted access.

Next, let's define a sample external property:

server.default.address.ip=192.168.0.2

Finally, to tell Spring to use the ServerConfigFactory class when loading the ApplicationContext (hence, create our configuration bean), we'll add the @ContextConfiguration annotation to the test class:

@ExtendWith(SpringExtension.class)
@EnableConfigurationProperties(value = ServerConfig.class)
@ContextConfiguration(classes = ServerConfigFactory.class)
@TestPropertySource("classpath:server-config-test.properties")
public class BindingPropertiesToBeanMethodsUnitTest {

    @Autowired
    @Qualifier("default_bean")
    private ServerConfig serverConfig;
    
    @Test
    void givenBeanAnnotatedMethod_whenBindingProperties_thenAllFieldsAreSet() {
        assertEquals("192.168.0.2", serverConfig.getAddress().getIp());

        // other assertions...
    }
}

5. Properties Validation

To enable bean validation in Spring Boot, we must annotate the top-level class with @Validated. Then, we add the required javax.validation constraints:

@Configuration
@ConfigurationProperties(prefix = "validate")
@Validated
public class MailServer {

    @NotNull
    @NotEmpty
    private Map<String, @NotBlank String> propertiesMap;

    @Valid
    private MailConfig mailConfig = new MailConfig();

    // getters and setters
}

Similarly, the MailConfig class also has some constraints:

public class MailConfig {

    @NotBlank
    @Email
    private String address;

    // getters and setters
}

By providing a valid data set:

validate.propertiesMap.first=prop1
validate.propertiesMap.second=prop2
[email protected]

the application will start normally and our unit tests will pass:

@ExtendWith(SpringExtension.class)
@EnableConfigurationProperties(value = MailServer.class)
@TestPropertySource("classpath:property-validation-test.properties")
public class PropertyValidationUnitTest {

    @Autowired
    private MailServer mailServer;

    private static Validator propertyValidator;

    @BeforeAll
    public static void setup() {
        propertyValidator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    @Test
    void whenBindingPropertiesToValidatedBeans_thenConstrainsAreChecked() {
        assertEquals(0, propertyValidator.validate(mailServer.getPropertiesMap()).size());
        assertEquals(0, propertyValidator.validate(mailServer.getMailConfig()).size());
    }
}

On the other hand, if we use invalid properties, Spring will throw an IllegalStateException at start-up.

For instance, using any of these invalid configurations:

validate.propertiesMap.second=
validate.mail_config.address=user1.test

will cause our application to fail, with this error message:

Property: validate.propertiesMap[second]
Value:
Reason: must not be blank

Property: validate.mailConfig.address
Value: user1.test
Reason: must be a well-formed email address

Notice that we've used @Valid on the mailConfig field to ensure that the MailConfig constraints are checked, even if validate.mailConfig.address wasn't defined. Otherwise, Spring would set mailConfig to null and start the application normally.

6. Properties Conversion

Spring Boot properties conversion enables us to convert some properties into specific types.

In this section, we'll start by testing configuration classes that use Spring's built-in conversion. Then, we'll test a custom converter that we create ourselves.

6.1. Spring Boot's Default Conversion

Let's consider the following data size and duration properties:

# data sizes
convert.upload_speed=500MB
convert.download_speed=10

# durations
convert.backup_day=1d
convert.backup_hour=8

Spring Boot will automatically bind these properties to the matching DataSize and Duration fields defined in the PropertyConversion configuration class:

@Configuration
@ConfigurationProperties(prefix = "convert")
public class PropertyConversion {

    private DataSize uploadSpeed;

    @DataSizeUnit(DataUnit.GIGABYTES)
    private DataSize downloadSpeed;

    private Duration backupDay;

    @DurationUnit(ChronoUnit.HOURS)
    private Duration backupHour;

    // getters and setters
}

Now, let's check the conversion results:

@ExtendWith(SpringExtension.class)
@EnableConfigurationProperties(value = PropertyConversion.class)
@ContextConfiguration(classes = CustomCredentialsConverter.class)
@TestPropertySource("classpath:spring-conversion-test.properties")
public class SpringPropertiesConversionUnitTest {

    @Autowired
    private PropertyConversion propertyConversion;

    @Test
    void whenUsingSpringDefaultSizeConversion_thenDataSizeObjectIsSet() {
        assertEquals(DataSize.ofMegabytes(500), propertyConversion.getUploadSpeed());
        assertEquals(DataSize.ofGigabytes(10), propertyConversion.getDownloadSpeed());
    }

    @Test
    void whenUsingSpringDefaultDurationConversion_thenDurationObjectIsSet() {
        assertEquals(Duration.ofDays(1), propertyConversion.getBackupDay());
        assertEquals(Duration.ofHours(8), propertyConversion.getBackupHour());
    }
}

6.2. Custom Converters

Now let's imagine that we want to convert the convert.credentials property:

convert.credentials=user,123

into the following Credential class:

public class Credentials {

    private String username;
    private String password;

    // getters and setters
}

To achieve this, we can implement a custom converter:

@Component
@ConfigurationPropertiesBinding
public class CustomCredentialsConverter implements Converter<String, Credentials> {

    @Override
    public Credentials convert(String source) {
        String[] data = source.split(",");
        return new Credentials(data[0], data[1]);
    }
}

Finally, let's add a Credentials field to the PropertyConversion class:

public class PropertyConversion {
    private Credentials credentials;
    // ...
}

In our SpringPropertiesConversionUnitTest test class, we also need to add @ContextConfiguration to register the custom converter in Spring's context:

// other annotations
@ContextConfiguration(classes=CustomCredentialsConverter.class)
public class SpringPropertiesConversionUnitTest {
    
    //...
    
    @Test
    void whenRegisteringCustomCredentialsConverter_thenCredentialsAreParsed() {
        assertEquals("user", propertyConversion.getCredentials().getUsername());
        assertEquals("123", propertyConversion.getCredentials().getPassword());
    }
}

As the previous assertions show, Spring has used our custom converter to parse the convert.credentials property into a Credentials instance.

7. YAML Documents Binding

For hierarchical configuration data, YAML configuration could be more convenient. Additionally, YAML supports defining multiple profiles inside the same document.

The following application.yml located under src/test/resources/ defines a “test” profile for the ServerConfig class:

spring:
   profiles: test
server:
   address:
      ip: 192.168.0.4
   resources_path:
      imgs: /etc/test/imgs
---
# other profiles

As a result, the following test will pass:

@ExtendWith(SpringExtension.class)
@ContextConfiguration(initializers = ConfigFileApplicationContextInitializer.class)
@EnableConfigurationProperties(value = ServerConfig.class)
@ActiveProfiles("test")
public class BindingYMLPropertiesUnitTest {

    @Autowired
    private ServerConfig serverConfig;

    @Test
    void whenBindingYMLConfigFile_thenAllFieldsAreSet() {
        assertEquals("192.168.0.4", serverConfig.getAddress().getIp());

        // other assertions ...
    }
}

A couple of notes regarding the used annotations:

  • @ContextConfiguration(initializers = ConfigFileApplicationContextInitializer.class) – loads the application.yml file
  • @ActiveProfiles(“test”) – specifies that the “test” profile will be used during this test

Finally, let's keep in mind that neither @ProperySource nor @TestProperySource support loading .yml files. Therefore, we should always place our YAML configurations within the application.yml file.

8. Overriding @ConfigurationProperties Configurations

Sometimes, we might want to override configuration properties loaded by @ConfigurationProperties with another data set, particularly when testing.

As we've shown in previous examples, we can use @TestPropertySource(“path_to_new_data_set”) to replace the whole original configuration (under /src/main/resources) with a new one.

Alternatively, we could selectively replace some of the original properties using the properties attribute of @TestPropertySource as well.

Suppose we want to override the previously defined validate.mail_config.address property with another value. All we have to do is to annotate our test class with @TestPropertySource and then assign a new value to the same property via the properties list:

@TestPropertySource(properties = {"[email protected]"})

Consequently, Spring will use the newly defined value:

assertEquals("[email protected]", mailServer.getMailConfig().getAddress());

9. Conclusion

In this tutorial, we've seen how to test different types of configuration classes that make use of the @ConfigurationProperties annotation to load .properties and .yml configuration files.

As usual, the source code for this article is available over on GitHub.

Generic bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
2 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Nils Breunese
Nils Breunese
8 months ago

Spring Boot provides dependency management for hibernate-validator and the other dependencies mentioned, so I would recommend to not specify versions for those in your own project. This will automatically keep those dependencies up-to-date with compatible versions.

Loredana Crusoveanu
8 months ago
Reply to  Nils Breunese

Thanks for the feedback.

I’ve updated the hibernate-validator definition to remove the dependency version and let it be managed by Boot. But for the javax.el dependencies, these are not managed by Boot as far as I can tell.

Comments are closed on this article!