1. 概述

在之前的 @ConfigurationProperties 使用指南 中,我们已经学习了如何在 Spring Boot 中使用 @ConfigurationProperties 注解来加载外部配置。

本文将重点讨论:如何对使用 @ConfigurationProperties 的配置类进行单元测试,确保配置项能正确绑定到对应的 Java 字段上。这是实际开发中容易踩坑的环节,尤其在复杂配置或嵌套结构下,光靠启动应用验证效率太低。

2. 依赖配置

在 Maven 项目中,我们需要引入以下两个核心依赖:

✅ **spring-boot-starter-test**:提供 Spring 测试支持(JUnit 5、Mockito 等)
✅ **spring-boot-starter-validation**:启用 Bean Validation(用于配置项校验)

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

⚠️ 注意:spring-boot-starter-validation 虽然是可选的,但一旦你在配置类中使用了 @NotNull@Email 等注解,就必须引入,否则校验不会生效。

3. 将配置绑定到自定义 POJO

通常我们会为配置项创建对应的 POJO 类,Spring Boot 会自动完成属性绑定。

假设我们有如下测试配置文件 src/test/resources/server-config-test.properties

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

对应的配置类定义如下:

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

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

    // getters and setters
}

其中 Address 是一个简单的嵌套类:

public class Address {
    private String ip;
    // getters and setters
}

接下来编写测试类,验证配置是否正确加载:

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

关键注解说明:

  • @ExtendWith(SpringExtension.class):让 JUnit 5 支持 Spring 测试上下文
  • @EnableConfigurationProperties:启用 @ConfigurationProperties 功能,注册指定的配置 Bean
  • @TestPropertySource:指定测试用的 properties 文件路径,优先级高于默认 application.properties

4. 在 @Bean 方法上使用 @ConfigurationProperties

除了直接在类上标注,还可以将 @ConfigurationProperties 加在 @Bean 方法上。这种方式特别适合对接第三方库或无法修改源码的类

例如,我们通过工厂类创建配置 Bean:

@Configuration
public class ServerConfigFactory {

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

对应的测试配置:

server.default.address.ip=192.168.0.2

测试类需要通过 @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());
        // 其他断言...
    }
}

⚠️ 注意:这里必须使用 @Qualifier("default_bean") 来注入指定名称的 Bean,否则可能因类型冲突导致注入失败。

5. 配置项校验(Validation)

Spring Boot 支持对 @ConfigurationProperties 类进行自动校验,但需要两个前提:

  1. 类上添加 @Validated
  2. 字段上使用 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
}

嵌套类也需定义校验规则:

public class MailConfig {
    @NotBlank
    @Email
    private String address;
    // getters and setters
}

测试用的合法配置:

validate.propertiesMap.first=prop1
validate.propertiesMap.second=prop2
validate.mail_config.address=user1@test.com

测试代码:

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

❌ 如果配置非法,例如:

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

Spring 启动时会直接抛出 IllegalStateException,错误信息如下:

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

关键点:嵌套对象必须加上 @Valid,否则即使字段为空也不会触发校验,Spring 会直接设为 null 并静默通过。

6. 配置项类型转换

Spring Boot 内置了丰富的类型转换机制,也支持自定义转换器。

6.1 Spring Boot 内置转换

支持自动转换 DataSizeDuration 等类型。

配置示例:

# 数据大小
convert.upload_speed=500MB
convert.download_speed=10

# 时间间隔
convert.backup_day=1d
convert.backup_hour=8

对应配置类:

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

测试代码验证转换结果:

@ExtendWith(SpringExtension.class)
@EnableConfigurationProperties(value = PropertyConversion.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 自定义转换器

如果需要将字符串转换为自定义对象(如 Credentials),可实现 Converter<String, T>

需求:将 user,123 转为 Credentials 对象。

convert.credentials=user,123

目标类:

public class Credentials {
    private String username;
    private String password;
    // 构造函数、getters/setters
}

自定义转换器:

@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]);
    }
}

⚠️ 注意:

  • 必须加 @Component 注册为 Spring Bean
  • 必须加 @ConfigurationPropertiesBinding 标识这是配置绑定专用转换器

更新 PropertyConversion 类:

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

测试时需通过 @ContextConfiguration 加载转换器:

@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 whenRegisteringCustomCredentialsConverter_thenCredentialsAreParsed() {
        assertEquals("user", propertyConversion.getCredentials().getUsername());
        assertEquals("123", propertyConversion.getCredentials().getPassword());
    }
}

7. YAML 配置绑定

对于层级复杂或需要多环境的配置,YAML 更清晰。它还支持多 profile 定义。

测试用的 src/test/resources/application.yml

spring:
  config:
    activate:
      on-profile: test
server:
  address:
    ip: 192.168.0.4
  resources_path:
    imgs: /etc/test/imgs
---
# 其他 profile

测试类写法:

@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());
        // 其他断言 ...
    }
}

关键点:

  • @ActiveProfiles("test"):激活 test profile
  • @ContextConfiguration(initializers = ...):手动加载 application.yml
  • @TestPropertySource 不支持 .yml 文件,只能用于 .properties

8. 覆盖配置项

测试时经常需要覆盖部分配置,有两种方式:

方式一:替换整个配置文件

@TestPropertySource("classpath:test-overrides.properties")

方式二:仅覆盖特定属性(推荐)

使用 properties 属性进行内联覆盖:

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

这样就能在不修改文件的情况下动态调整配置,简单粗暴有效。

9. 总结

本文系统梳理了 @ConfigurationProperties 的测试方法,涵盖:

  • ✅ 基础 POJO 绑定测试
  • @Bean 方法绑定场景
  • ✅ 配置校验与异常触发
  • ✅ 内置与自定义类型转换
  • ✅ YAML 多 profile 支持
  • ✅ 测试时配置覆盖技巧

这些是保障配置正确性的关键手段,建议在项目中建立标准测试模板,避免因配置错误导致线上问题。

示例代码已托管至 GitHub:https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/spring-boot-testing


原始标题:Testing Spring Boot @ConfigurationProperties | Baeldung