unit-test-config-properties by giuseppe-trisciuoglio/developer-kit
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-config-properties本技能提供了使用 JUnit 5 对 @ConfigurationProperties 绑定、环境特定配置和属性验证进行单元测试的模式。它涵盖了测试属性名称映射、类型转换、验证约束、嵌套结构以及特定于配置文件的配置,而无需启动完整的 Spring 上下文。
在以下情况下使用此技能:
@ConfigurationProperties 属性绑定@Validated 的属性在值无效时失败@Profile 测试环境特定配置广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
@<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.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
dependencies {
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.assertj:assertj-core")
}
// Configuration properties class
@ConfigurationProperties(prefix = "app.security")
@Data
public class SecurityProperties {
private String jwtSecret;
private long jwtExpirationMs;
private int maxLoginAttempts;
private boolean enableTwoFactor;
}
// Unit test
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.*;
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.database")
@Data
public class DatabaseProperties {
private String url;
private String username;
private Pool pool = new Pool();
private List<Replica> replicas = new ArrayList<>();
@Data
public static class Pool {
private int maxSize = 10;
private int minIdle = 5;
private long connectionTimeout = 30000;
}
@Data
public static class Replica {
private String name;
private String url;
private int priority;
}
}
class NestedPropertiesTest {
@Test
void shouldBindNestedProperties() {
new ApplicationContextRunner()
.withPropertyValues(
"app.database.url=jdbc:mysql://localhost/db",
"app.database.username=admin",
"app.database.pool.maxSize=20",
"app.database.pool.minIdle=10",
"app.database.pool.connectionTimeout=60000"
)
.withBean(DatabaseProperties.class)
.run(context -> {
DatabaseProperties props = context.getBean(DatabaseProperties.class);
assertThat(props.getUrl()).isEqualTo("jdbc:mysql://localhost/db");
assertThat(props.getPool().getMaxSize()).isEqualTo(20);
assertThat(props.getPool().getConnectionTimeout()).isEqualTo(60000L);
});
}
@Test
void shouldBindListOfReplicas() {
new ApplicationContextRunner()
.withPropertyValues(
"app.database.replicas[0].name=replica-1",
"app.database.replicas[0].url=jdbc:mysql://replica1/db",
"app.database.replicas[0].priority=1",
"app.database.replicas[1].name=replica-2",
"app.database.replicas[1].url=jdbc:mysql://replica2/db",
"app.database.replicas[1].priority=2"
)
.withBean(DatabaseProperties.class)
.run(context -> {
DatabaseProperties props = context.getBean(DatabaseProperties.class);
assertThat(props.getReplicas()).hasSize(2);
assertThat(props.getReplicas().get(0).getName()).isEqualTo("replica-1");
assertThat(props.getReplicas().get(1).getPriority()).isEqualTo(2);
});
}
}
@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;
@Email
private String adminEmail;
}
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 shouldFailValidationWhenPortOutOfRange() {
new ApplicationContextRunner()
.withPropertyValues(
"app.server.host=localhost",
"app.server.port=99999",
"app.server.threadPoolSize=10"
)
.withBean(ServerProperties.class)
.run(context -> {
assertThat(context).hasFailed();
});
}
@Test
void shouldPassValidationWithValidConfiguration() {
new ApplicationContextRunner()
.withPropertyValues(
"app.server.host=localhost",
"app.server.port=8080",
"app.server.threadPoolSize=10",
"app.server.adminEmail=admin@example.com"
)
.withBean(ServerProperties.class)
.run(context -> {
assertThat(context).hasNotFailed();
ServerProperties props = context.getBean(ServerProperties.class);
assertThat(props.getHost()).isEqualTo("localhost");
});
}
}
@Configuration
@Profile("prod")
class ProductionConfiguration {
@Bean
public SecurityProperties securityProperties() {
SecurityProperties props = new SecurityProperties();
props.setEnableTwoFactor(true);
props.setMaxLoginAttempts(3);
return props;
}
}
@Configuration
@Profile("dev")
class DevelopmentConfiguration {
@Bean
public SecurityProperties securityProperties() {
SecurityProperties props = new SecurityProperties();
props.setEnableTwoFactor(false);
props.setMaxLoginAttempts(999);
return props;
}
}
class ProfileBasedConfigurationTest {
@Test
void shouldLoadProductionConfiguration() {
new ApplicationContextRunner()
.withPropertyValues("spring.profiles.active=prod")
.withUserConfiguration(ProductionConfiguration.class)
.run(context -> {
SecurityProperties props = context.getBean(SecurityProperties.class);
assertThat(props.isEnableTwoFactor()).isTrue();
assertThat(props.getMaxLoginAttempts()).isEqualTo(3);
});
}
@Test
void shouldLoadDevelopmentConfiguration() {
new ApplicationContextRunner()
.withPropertyValues("spring.profiles.active=dev")
.withUserConfiguration(DevelopmentConfiguration.class)
.run(context -> {
SecurityProperties props = context.getBean(SecurityProperties.class);
assertThat(props.isEnableTwoFactor()).isFalse();
assertThat(props.getMaxLoginAttempts()).isEqualTo(999);
});
}
}
@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;
private Charset fileEncoding = StandardCharsets.UTF_8;
}
class TypeConversionTest {
@Test
void shouldConvertStringToDuration() {
new ApplicationContextRunner()
.withPropertyValues("app.features.cacheExpiry=30s")
.withBean(FeatureProperties.class)
.run(context -> {
FeatureProperties props = context.getBean(FeatureProperties.class);
assertThat(props.getCacheExpiry()).isEqualTo(Duration.ofSeconds(30));
});
}
@Test
void shouldConvertStringToDataSize() {
new ApplicationContextRunner()
.withPropertyValues("app.features.maxUploadSize=50MB")
.withBean(FeatureProperties.class)
.run(context -> {
FeatureProperties props = context.getBean(FeatureProperties.class);
assertThat(props.getMaxUploadSize()).isEqualTo(DataSize.ofMegabytes(50));
});
}
@Test
void shouldConvertCommaDelimitedListToList() {
new ApplicationContextRunner()
.withPropertyValues("app.features.enabledFeatures=feature1,feature2,feature3")
.withBean(FeatureProperties.class)
.run(context -> {
FeatureProperties props = context.getBean(FeatureProperties.class);
assertThat(props.getEnabledFeatures())
.containsExactly("feature1", "feature2", "feature3");
});
}
}
@ConfigurationProperties(prefix = "app.cache")
@Data
public class CacheProperties {
private long ttlSeconds = 300;
private int maxSize = 1000;
private boolean enabled = true;
private String cacheType = "IN_MEMORY";
}
class DefaultValuesTest {
@Test
void shouldUseDefaultValuesWhenNotSpecified() {
new ApplicationContextRunner()
.withBean(CacheProperties.class)
.run(context -> {
CacheProperties props = context.getBean(CacheProperties.class);
assertThat(props.getTtlSeconds()).isEqualTo(300L);
assertThat(props.getMaxSize()).isEqualTo(1000);
assertThat(props.isEnabled()).isTrue();
assertThat(props.getCacheType()).isEqualTo("IN_MEMORY");
});
}
@Test
void shouldOverrideDefaultValuesWithProvidedProperties() {
new ApplicationContextRunner()
.withPropertyValues(
"app.cache.ttlSeconds=600",
"app.cache.cacheType=REDIS"
)
.withBean(CacheProperties.class)
.run(context -> {
CacheProperties props = context.getBean(CacheProperties.class);
assertThat(props.getTtlSeconds()).isEqualTo(600L);
assertThat(props.getCacheType()).isEqualTo("REDIS");
assertThat(props.getMaxSize()).isEqualTo(1000); // Default unchanged
});
}
}
@Validated:添加 @Validated 以启用对配置属性的验证@ConstructorBinding 限制:使用 @ConstructorBinding 时,所有参数都必须是可绑定的属性未绑定:验证前缀和属性名称是否完全匹配(包括短横线命名法到驼峰命名法的转换)。
验证未触发:确保存在 @Validated 并且验证依赖项在类路径上。
未找到 ApplicationContextRunner:验证 spring-boot-starter-test 是否在测试依赖项中。
每周安装次数
333
仓库
GitHub 星标数
173
首次出现
2026年2月3日
安全审计
已安装于
claude-code265
gemini-cli251
opencode250
cursor247
codex245
github-copilot231
This skill provides patterns for unit testing @ConfigurationProperties bindings, environment-specific configurations, and property validation using JUnit 5. It covers testing property name mapping, type conversions, validation constraints, nested structures, and profile-specific configurations without full Spring context startup.
Use this skill when:
@ConfigurationProperties property binding@Validated properties fail with invalid values@Profile to test environment-specific configurations@ConfigurationProperties matches test properties<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.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
dependencies {
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.assertj:assertj-core")
}
// Configuration properties class
@ConfigurationProperties(prefix = "app.security")
@Data
public class SecurityProperties {
private String jwtSecret;
private long jwtExpirationMs;
private int maxLoginAttempts;
private boolean enableTwoFactor;
}
// Unit test
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.*;
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.database")
@Data
public class DatabaseProperties {
private String url;
private String username;
private Pool pool = new Pool();
private List<Replica> replicas = new ArrayList<>();
@Data
public static class Pool {
private int maxSize = 10;
private int minIdle = 5;
private long connectionTimeout = 30000;
}
@Data
public static class Replica {
private String name;
private String url;
private int priority;
}
}
class NestedPropertiesTest {
@Test
void shouldBindNestedProperties() {
new ApplicationContextRunner()
.withPropertyValues(
"app.database.url=jdbc:mysql://localhost/db",
"app.database.username=admin",
"app.database.pool.maxSize=20",
"app.database.pool.minIdle=10",
"app.database.pool.connectionTimeout=60000"
)
.withBean(DatabaseProperties.class)
.run(context -> {
DatabaseProperties props = context.getBean(DatabaseProperties.class);
assertThat(props.getUrl()).isEqualTo("jdbc:mysql://localhost/db");
assertThat(props.getPool().getMaxSize()).isEqualTo(20);
assertThat(props.getPool().getConnectionTimeout()).isEqualTo(60000L);
});
}
@Test
void shouldBindListOfReplicas() {
new ApplicationContextRunner()
.withPropertyValues(
"app.database.replicas[0].name=replica-1",
"app.database.replicas[0].url=jdbc:mysql://replica1/db",
"app.database.replicas[0].priority=1",
"app.database.replicas[1].name=replica-2",
"app.database.replicas[1].url=jdbc:mysql://replica2/db",
"app.database.replicas[1].priority=2"
)
.withBean(DatabaseProperties.class)
.run(context -> {
DatabaseProperties props = context.getBean(DatabaseProperties.class);
assertThat(props.getReplicas()).hasSize(2);
assertThat(props.getReplicas().get(0).getName()).isEqualTo("replica-1");
assertThat(props.getReplicas().get(1).getPriority()).isEqualTo(2);
});
}
}
@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;
@Email
private String adminEmail;
}
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 shouldFailValidationWhenPortOutOfRange() {
new ApplicationContextRunner()
.withPropertyValues(
"app.server.host=localhost",
"app.server.port=99999",
"app.server.threadPoolSize=10"
)
.withBean(ServerProperties.class)
.run(context -> {
assertThat(context).hasFailed();
});
}
@Test
void shouldPassValidationWithValidConfiguration() {
new ApplicationContextRunner()
.withPropertyValues(
"app.server.host=localhost",
"app.server.port=8080",
"app.server.threadPoolSize=10",
"app.server.adminEmail=admin@example.com"
)
.withBean(ServerProperties.class)
.run(context -> {
assertThat(context).hasNotFailed();
ServerProperties props = context.getBean(ServerProperties.class);
assertThat(props.getHost()).isEqualTo("localhost");
});
}
}
@Configuration
@Profile("prod")
class ProductionConfiguration {
@Bean
public SecurityProperties securityProperties() {
SecurityProperties props = new SecurityProperties();
props.setEnableTwoFactor(true);
props.setMaxLoginAttempts(3);
return props;
}
}
@Configuration
@Profile("dev")
class DevelopmentConfiguration {
@Bean
public SecurityProperties securityProperties() {
SecurityProperties props = new SecurityProperties();
props.setEnableTwoFactor(false);
props.setMaxLoginAttempts(999);
return props;
}
}
class ProfileBasedConfigurationTest {
@Test
void shouldLoadProductionConfiguration() {
new ApplicationContextRunner()
.withPropertyValues("spring.profiles.active=prod")
.withUserConfiguration(ProductionConfiguration.class)
.run(context -> {
SecurityProperties props = context.getBean(SecurityProperties.class);
assertThat(props.isEnableTwoFactor()).isTrue();
assertThat(props.getMaxLoginAttempts()).isEqualTo(3);
});
}
@Test
void shouldLoadDevelopmentConfiguration() {
new ApplicationContextRunner()
.withPropertyValues("spring.profiles.active=dev")
.withUserConfiguration(DevelopmentConfiguration.class)
.run(context -> {
SecurityProperties props = context.getBean(SecurityProperties.class);
assertThat(props.isEnableTwoFactor()).isFalse();
assertThat(props.getMaxLoginAttempts()).isEqualTo(999);
});
}
}
@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;
private Charset fileEncoding = StandardCharsets.UTF_8;
}
class TypeConversionTest {
@Test
void shouldConvertStringToDuration() {
new ApplicationContextRunner()
.withPropertyValues("app.features.cacheExpiry=30s")
.withBean(FeatureProperties.class)
.run(context -> {
FeatureProperties props = context.getBean(FeatureProperties.class);
assertThat(props.getCacheExpiry()).isEqualTo(Duration.ofSeconds(30));
});
}
@Test
void shouldConvertStringToDataSize() {
new ApplicationContextRunner()
.withPropertyValues("app.features.maxUploadSize=50MB")
.withBean(FeatureProperties.class)
.run(context -> {
FeatureProperties props = context.getBean(FeatureProperties.class);
assertThat(props.getMaxUploadSize()).isEqualTo(DataSize.ofMegabytes(50));
});
}
@Test
void shouldConvertCommaDelimitedListToList() {
new ApplicationContextRunner()
.withPropertyValues("app.features.enabledFeatures=feature1,feature2,feature3")
.withBean(FeatureProperties.class)
.run(context -> {
FeatureProperties props = context.getBean(FeatureProperties.class);
assertThat(props.getEnabledFeatures())
.containsExactly("feature1", "feature2", "feature3");
});
}
}
@ConfigurationProperties(prefix = "app.cache")
@Data
public class CacheProperties {
private long ttlSeconds = 300;
private int maxSize = 1000;
private boolean enabled = true;
private String cacheType = "IN_MEMORY";
}
class DefaultValuesTest {
@Test
void shouldUseDefaultValuesWhenNotSpecified() {
new ApplicationContextRunner()
.withBean(CacheProperties.class)
.run(context -> {
CacheProperties props = context.getBean(CacheProperties.class);
assertThat(props.getTtlSeconds()).isEqualTo(300L);
assertThat(props.getMaxSize()).isEqualTo(1000);
assertThat(props.isEnabled()).isTrue();
assertThat(props.getCacheType()).isEqualTo("IN_MEMORY");
});
}
@Test
void shouldOverrideDefaultValuesWithProvidedProperties() {
new ApplicationContextRunner()
.withPropertyValues(
"app.cache.ttlSeconds=600",
"app.cache.cacheType=REDIS"
)
.withBean(CacheProperties.class)
.run(context -> {
CacheProperties props = context.getBean(CacheProperties.class);
assertThat(props.getTtlSeconds()).isEqualTo(600L);
assertThat(props.getCacheType()).isEqualTo("REDIS");
assertThat(props.getMaxSize()).isEqualTo(1000); // Default unchanged
});
}
}
@Validated: Add @Validated to enable validation on configuration properties@ConstructorBinding limitations: When using @ConstructorBinding, all parameters must be bindableProperties not binding : Verify prefix and property names match exactly (including kebab-case to camelCase conversion).
Validation not triggered : Ensure @Validated is present and validation dependencies are on classpath.
ApplicationContextRunner not found : Verify spring-boot-starter-test is in test dependencies.
Weekly Installs
333
Repository
GitHub Stars
173
First Seen
Feb 3, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
claude-code265
gemini-cli251
opencode250
cursor247
codex245
github-copilot231
Vue.js测试最佳实践:Vue 3组件、组合式函数、Pinia与异步测试完整指南
3,700 周安装