mcpField
U
unit-test-config-properties

unit-test-config-properties

github.com/giuseppe-trisciuoglio测试与质量3653.7k 次安装7 周前更新
代码仓库

关于 unit-test-config-properties

unit-test-config-properties 是一项使用 JUnit 5 测试 Spring 配置属性和环境配置的技能,适合需要快速验证绑定与校验逻辑的开发者。它介绍使用 ApplicationContextRunner,无需启动完整 Spring 上下文即可测试类型转换、嵌套结构、默认值和 Profile 配置。

主要功能

  1. 01测试配置属性绑定与映射
  2. 02验证约束和无效值处理
  3. 03检查 Duration、DataSize 等类型转换
  4. 04覆盖嵌套结构与集合
  5. 05测试默认值和 Profile 配置

适用场景

  1. 01验证 @ConfigurationProperties 属性绑定
  2. 02检查配置校验是否拒绝无效值
  3. 03确认 Duration 和集合转换结果
  4. 04分别测试 dev、prod 配置

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-config-properties

安装后在 Claude Code 中输入/unit-test-config-properties确认已加载。

附带文件references/advanced-examples.md

详细介绍

单元测试配置属性和配置文件

概述

本技能提供使用 JUnit 5 对 @ConfigurationProperties 绑定、环境专属配置和属性验证进行单元测试的模式。涵盖属性名称映射、类型转换、验证约束、嵌套结构和特定配置文件的配置测试,无需启动完整的 Spring 上下文。

关键验证检查点:

  • @ConfigurationProperties 与测试属性中的属性前缀一致
  • 对无效值进行验证时,带有 @Validated 的类会触发验证
  • Duration、DataSize、集合和映射的类型转换正常

适用场景

  • 测试 @ConfigurationProperties 属性绑定
  • 测试属性名称映射和类型转换
  • 使用 @NotBlank、@Min、@Max、@Email 约束验证配置
  • 测试特定环境的配置(dev、prod)
  • 测试嵌套属性结构和集合
  • 验证未指定属性时的默认值
  • 无需启动 Spring 上下文即可快速测试配置

操作说明

  1. 设置测试依赖项:添加 spring-boot-starter-test 和 AssertJ 依赖项
  2. 使用 ApplicationContextRunner:无需启动完整的 Spring 上下文即可测试属性绑定
  3. 定义属性前缀:确保 @ConfigurationProperties(prefix = "...") 与测试属性路径一致
  4. 测试所有属性路径:验证每个属性,包括嵌套结构和集合
  5. 测试验证约束:使用 context.hasFailed() 验证 @Validated 属性会拒绝无效值
  6. 测试类型转换:验证 Duration(30s)、DataSize(50MB)、集合和映射能正确转换
  7. 测试默认值:验证测试属性中未指定的属性具有正确的默认值
  8. 测试特定配置文件的配置:将 @Profile 与 ApplicationContextRunner 配合使用,测试特定环境的配置
  9. 测试边界情况:包含空字符串、null 值和类型不匹配的情况

故障排查流程:

  • 如果属性未绑定 → 检查前缀是否一致(kebab-case 到 camelCase 的转换)
  • 如果验证未触发 → 确认存在 @Validated 注解
  • 如果上下文启动失败 → 检查依赖项和 @ConfigurationProperties 类的结构

示例

设置:测试依赖项
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <scope>test</scope>
</dependency>
基本模式:属性绑定
@ConfigurationProperties(prefix = "app.security")
@Data
public class SecurityProperties {
  private String jwtSecret;
  private long jwtExpirationMs;
  private int maxLoginAttempts;
  private boolean enableTwoFactor;
}

class SecurityPropertiesTest {

  @Test
  void shouldBindPropertiesFromEnvironment() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.security.jwtSecret=my-secret-key",
        "app.security.jwtExpirationMs=3600000",
        "app.security.maxLoginAttempts=5",
        "app.security.enableTwoFactor=true"
      )
      .withBean(SecurityProperties.class)
      .run(context -> {
        SecurityProperties props = context.getBean(SecurityProperties.class);
        assertThat(props.getJwtSecret()).isEqualTo("my-secret-key");
        assertThat(props.getJwtExpirationMs()).isEqualTo(3600000L);
        assertThat(props.getMaxLoginAttempts()).isEqualTo(5);
        assertThat(props.isEnableTwoFactor()).isTrue();
      });
  }

  @Test
  void shouldUseDefaultValuesWhenPropertiesNotProvided() {
    new ApplicationContextRunner()
      .withPropertyValues("app.security.jwtSecret=key")
      .withBean(SecurityProperties.class)
      .run(context -> {
        SecurityProperties props = context.getBean(SecurityProperties.class);
        assertThat(props.getJwtSecret()).isEqualTo("key");
        assertThat(props.getMaxLoginAttempts()).isZero();
      });
  }
}
验证测试
@ConfigurationProperties(prefix = "app.server")
@Data
@Validated
public class ServerProperties {
  @NotBlank
  private String host;

  @Min(1)
  @Max(65535)
  private int port = 8080;

  @Positive
  private int threadPoolSize;
}

class ConfigurationValidationTest {

  @Test
  void shouldFailValidationWhenHostIsBlank() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.server.host=",
        "app.server.port=8080",
        "app.server.threadPoolSize=10"
      )
      .withBean(ServerProperties.class)
      .run(context -> {
        assertThat(context).hasFailed()
          .getFailure()
          .hasMessageContaining("host");
      });
  }

  @Test
  void shouldPassValidationWithValidConfiguration() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.server.host=localhost",
        "app.server.port=8080",
        "app.server.threadPoolSize=10"
      )
      .withBean(ServerProperties.class)
      .run(context -> {
        assertThat(context).hasNotFailed();
        assertThat(context.getBean(ServerProperties.class).getHost()).isEqualTo("localhost");
      });
  }
}
类型转换测试
@ConfigurationProperties(prefix = "app.features")
@Data
public class FeatureProperties {
  private Duration cacheExpiry = Duration.ofMinutes(10);
  private DataSize maxUploadSize = DataSize.ofMegabytes(100);
  private List<String> enabledFeatures;
  private Map<String, String> featureFlags;
}

class TypeConversionTest {

  @Test
  void shouldConvertDurationFromString() {
    new ApplicationContextRunner()
      .withPropertyValues("app.features.cacheExpiry=30s")
      .withBean(FeatureProperties.class)
      .run(context -> {
        assertThat(context.getBean(FeatureProperties.class).getCacheExpiry())
          .isEqualTo(Duration.ofSeconds(30));
      });
  }

  @Test
  void shouldConvertCommaDelimitedList() {
    new ApplicationContextRunner()
      .withPropertyValues("app.features.enabledFeatures=feature1,feature2")
      .withBean(FeatureProperties.class)
      .run(context -> {
        assertThat(context.getBean(FeatureProperties.class).getEnabledFeatures())
          .containsExactly("feature1", "feature2");
      });
  }
}

有关嵌套属性、特定配置文件的配置、集合绑定和高级验证模式,请参阅 references/advanced-examples.md。

最佳实践

  • 测试所有属性绑定,包括嵌套结构和集合
  • 测试所有验证约束,包括 @NotBlank、@Min、@Max、@Email、@Positive 注解
  • 测试默认值和自定义值,验证回退行为
  • 使用 ApplicationContextRunner 进行快速的无上下文测试
  • 使用 @Profile 分别测试特定配置文件的配置
  • 验证 Duration、DataSize、集合和映射的类型转换
  • 测试边界情况:空字符串、null 值、类型不匹配和超出范围的值

限制与警告

  • Kebab-case 转 camelCase:属性 app.my-property 在 Java 中映射为 myProperty
  • 宽松绑定:Spring Boot 默认使用宽松绑定;如有需要,请使用严格绑定
  • 必须使用 @Validated:添加 @Validated 注解以启用约束验证
  • @ConstructorBinding:使用构造函数绑定时,所有参数都必须可绑定
  • 列表索引:使用 [0]、[1] 表示法;确保列表索引连续
  • Duration 格式:接受 ISO-8601(PT30S)或简单语法(30s、1m、2h)
  • 上下文隔离:每个 ApplicationContextRunner 都会创建一个不共享状态的新上下文
  • 配置文件激活:在 withPropertyValues() 中使用 spring.profiles.active=profileName 进行配置文件测试

故障排查

问题原因解决方案
属性未绑定前缀不匹配确认 @ConfigurationProperties(prefix="...") 与属性路径匹配
未触发验证缺少 @Validated在配置类上添加 @Validated 注解
上下文启动失败缺少依赖项确保 spring-boot-starter-test 位于测试作用域
嵌套属性为 null缺少内部类在嵌套类上使用 @Data,或提供 getter/setter
集合绑定失败索引格式错误使用 [0]、[1] 表示法,而不是 (0)、(1)

评论

支持 Markdown