Testing Spring Boot @ConfigurationProperties – 测试Spring Boot @ConfigurationProperties

最后修改: 2020年 3月 5日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

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

在我们之前的@ConfigurationProperties指南中,我们了解了如何在Spring Boot中设置和使用@ConfigurationProperties注,以便与外部配置协作。

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

在本教程中,我们将讨论如何测试依赖于@ConfigurationProperties注解的配置类,以确保我们的配置数据被加载并正确绑定到其相应的字段。

2. Dependencies

2.依赖性

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. Additionally, we’ll use spring-boot-starter-validation as the bean validation dependency:

在我们的Maven项目中,我们将使用spring-boot-starterspring-boot-starter-test依赖项来启用spring核心API和Spring的测试API。此外,我们将使用spring-boot-starter-validation作为bean验证依赖。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.0</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>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

3. Properties Binding to User Defined POJOs

3.与用户定义的POJO绑定的属性

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.

在处理外部化配置时,我们通常会创建包含与匹配的配置属性对应的字段的POJO。正如我们已经知道的,Spring会自动将配置属性与我们创建的Java类绑定。

To start, let’s assume that we have some server configuration inside a properties file called src/test/resources/server-config-test.properties:

首先,让我们假设在一个名为src/test/resources/server-config-test.properties的属性文件内有一些服务器配置。

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

We’ll 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:

还有相应的Address类型。

public class Address {

    private String ip;

    // getters and setters
}

Finally, we’ll inject the ServerConfig POJO into our test class, and validate that all of its fields are set correctly:

最后,我们将把ServerConfig POJO注入到我们的测试类中,并验证其所有字段都被正确设置。

@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 used the following annotations:

在这个测试中,我们使用了以下注释。

  • @ExtendWith – integrates Spring’s TestContext framework with JUnit5
  • @EnableConfigurationProperties – enables support for @ConfigurationProperties beans (in this case, the ServerConfig bean)
  • @TestPropertySource – specifies a testing file that overrides the default application.properties file

4. @ConfigurationProperties on @Bean Methods

4.@ConfigurationProperties上的@Bean方法

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

创建配置Bean的另一种方式是在@Bean方法上使用@ConfigurationProperties注解

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

例如,下面的getDefaultConfigs()方法创建了一个ServerConfig配置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.

正如我们所见,我们能够使用@ConfigurationPropertiesgetDefaultConfigs()方法上配置ServerConfig实例,而无需编辑ServerConfig类本身。当与一个访问受限的外部第三方类合作时,这可能特别有帮助。

Next, we’ll 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 (thus creating our configuration bean), we’ll add the @ContextConfiguration annotation to the test class:

最后,为了告诉Spring在加载ApplicationContext时使用ServerConfigFactory类(从而创建我们的配置Bean),我们将在测试类中添加@ContextConfiguration注解。

@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

5.属性验证

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

要在Spring Boot中启用bean验证我们必须用@Validated注释顶层类。然后我们添加所需的javax.validation约束。

@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:

同样地,MailConfig类也有一些约束。

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
validate.mail_config.address=user1@test

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());
    }
}

Conversely, if we use invalid properties, Spring will throw an IllegalStateException at start-up.

相反,如果我们使用无效的属性,Spring将在启动时抛出一个IllegalStateException

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 used @Valid on the mailConfig field to ensure that the MailConfig constraints are checked, even if validate.mailConfig.address isn’t defined. Otherwise, Spring will set mailConfig to null and start the application normally.

请注意,我们在mailConfig字段上使用了@Valid,以确保MailConfig约束被检查,即使validate.mailConfig.address未被定义。否则,Spring将把mailConfig设置为null并正常启动应用程序。

6. Properties Conversion

6.属性转换

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

Spring Boot属性转换使我们能够将一些属性转换为特定类型。

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’ll create ourselves.

在本节中,我们将首先测试使用Spring内置转换的配置类。然后我们将测试一个我们自己创建的自定义转换器。

6.1. Spring Boot’s Default Conversion

6.1.Spring Boot的默认转换

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:

Spring Boot将自动把这些属性绑定到DataSizeDuration字段上,这些字段在PropertyConversion配置类中定义。

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

We’ll 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

6.2.自定义转换器

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

现在让我们设想一下,我们要转换convert.credentials属性。

convert.credentials=user,123

into the following Credential class:

进入以下Credential类。

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, we’ll add a Credentials field to the PropertyConversion class:

最后,我们将在PropertyConversion类中添加一个Credentials字段。

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:

在我们的SpringPropertiesConversionUnitTest测试类中,我们还需要添加@ContextConfiguration来在Spring的上下文中注册自定义转换器。

// 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.

正如前面的断言所示,Spring已经使用我们的自定义转换器将convert.credentials属性解析为Credentials实例

7. YAML Documents Binding

7.YAML文档的绑定

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

对于分层的配置数据,YAML配置可能更方便。YAML 还支持在同一个文档中定义多个配置文件。

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

位于src/test/resources/下的以下application.ymlServerConfig类定义了一个 “测试 “配置文件。

spring:
  config:
    activate:
      on-profile: 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 = ConfigDataApplicationContextInitializer.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 annotations we used:

关于我们使用的注释,有几点说明。

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

Finally, 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.

最后,请记住,@ProperySource@TestProperySource都不支持加载.yml文件因此,我们应该始终将YAML配置放在application.yml文件中

8. Overriding @ConfigurationProperties Configurations

8.重写@ConfigurationProperties配置

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

有时,我们可能想用另一个数据集来覆盖由@ConfigurationProperties加载的配置属性,特别是在测试时。

As we’ve seen 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.

正如我们在之前的例子中所看到的,我们可以使用@TestPropertySource(“path_to_new_data_set”)来用一个新的配置替换整个原始配置(在/src/main/resources)

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

另外,我们可以使用@TestPropertySourceproperties属性选择性地替换一些原始属性。

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

假设我们想用另一个值覆盖之前定义的validate.mail_config.address属性。我们所要做的就是用@TestPropertySource,来注释我们的测试类,然后通过properties列表给同一个属性分配一个新的值。

@TestPropertySource(properties = {"validate.mail_config.address=new_user@test"})

Consequently, Spring will use the newly defined value:

因此,Spring将使用新定义的值。

assertEquals("new_user@test", mailServer.getMailConfig().getAddress());

9. Conclusion

9.结语

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

在这篇文章中,我们学习了如何测试不同类型的配置类,它们利用@ConfigurationProperties注解来加载.properties.yml配置文件。

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

像往常一样,本文的源代码可以在GitHub上找到