关于 unit-test-config-properties
unit-test-config-properties 是一项使用 JUnit 5 测试 Spring 配置属性和环境配置的技能,适合需要快速验证绑定与校验逻辑的开发者。它介绍使用 ApplicationContextRunner,无需启动完整 Spring 上下文即可测试类型转换、嵌套结构、默认值和 Profile 配置。
主要功能
- 01测试配置属性绑定与映射
- 02验证约束和无效值处理
- 03检查 Duration、DataSize 等类型转换
- 04覆盖嵌套结构与集合
- 05测试默认值和 Profile 配置
适用场景
- 01验证 @ConfigurationProperties 属性绑定
- 02检查配置校验是否拒绝无效值
- 03确认 Duration 和集合转换结果
- 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 上下文即可快速测试配置
操作说明
- 设置测试依赖项:添加
spring-boot-starter-test和 AssertJ 依赖项 - 使用 ApplicationContextRunner:无需启动完整的 Spring 上下文即可测试属性绑定
- 定义属性前缀:确保
@ConfigurationProperties(prefix = "...")与测试属性路径一致 - 测试所有属性路径:验证每个属性,包括嵌套结构和集合
- 测试验证约束:使用
context.hasFailed()验证@Validated属性会拒绝无效值 - 测试类型转换:验证 Duration(
30s)、DataSize(50MB)、集合和映射能正确转换 - 测试默认值:验证测试属性中未指定的属性具有正确的默认值
- 测试特定配置文件的配置:将
@Profile与ApplicationContextRunner配合使用,测试特定环境的配置 - 测试边界情况:包含空字符串、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