安装
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-config-properties
复制
Unit Testing Configuration Properties and Profiles
Overview
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.
When to Use
Use this skill when:
Testing @ConfigurationProperties property binding
Testing property name mapping and type conversions
Verifying configuration validation
Testing environment-specific configurations
Testing nested property structures
Want fast configuration tests without Spring context
Instructions
Use ApplicationContextRunner
Test property bindings without starting full Spring context
Test all property paths
Verify each property including nested structures and collections
Test validation constraints
Ensure @Validated properties fail with invalid values
Test type conversions
Verify Duration, DataSize, and other special types convert correctly
Test default values
Verify properties have correct defaults when not specified
Test profile-specific configs
Use @Profile to test environment-specific configurations
Verify property prefixes
Ensure the prefix in @ConfigurationProperties matches test properties
Test edge cases
Include empty strings, null values, and type mismatches
Examples
Setup: Configuration Testing
Maven
<
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
>
Gradle
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"
)
}
Basic Pattern: Testing ConfigurationProperties
Simple Property Binding
// 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
(
)
;
}
)
;
}
}
Testing Nested Configuration Properties
Complex Property Structure
@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
)
;
}
)
;
}
}
Testing Property Validation
Validate Configuration with Constraints
@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"
)
;
}
)
;
}
}
Testing Profile-Specific Configurations
Environment-Specific Properties
@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
)
;
}
)
;
}
}
Testing Type Conversion
Property Type Binding
@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"
)
;
}
)
;
}
}
Testing Property Binding with Default Values
Verify Default Configuration
@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
}
)
;
}
}
Best Practices
Test all property bindings
including nested structures
Test validation constraints
thoroughly
Test both default and custom values
Use ApplicationContextRunner
for context-free testing
Test profile-specific configurations
separately
Verify type conversions
work correctly
Test edge cases
(empty strings, null values, type mismatches)
Common Pitfalls
Not testing validation constraints
Forgetting to test default values
Not testing nested property structures
Testing with wrong property prefix
Not handling type conversion properly
Constraints and Warnings
Property name matching
Kebab-case in properties (app.my-prop) maps to camelCase in Java (myProp)
Loose binding by default
Spring Boot supports loose binding; enable strict binding if needed
Validation requires @Validated
Add @Validated to enable validation on configuration properties
@ConstructorBinding limitations
When using @ConstructorBinding, all parameters must be bindable
List indexing
List properties use [0], [1] notation; ensure sequential indexing
Duration format
Duration properties accept standard ISO-8601 format or simple syntax (10s, 1m)
ApplicationContextRunner isolation
Each ApplicationContextRunner creates a new context; there's no shared state
Troubleshooting
Properties 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.
References
Spring Boot ConfigurationProperties
ApplicationContextRunner Testing
Spring Profiles
← 返回排行榜