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. Covers testing property name mapping, type conversions, validation constraints, nested structures, and profile-specific configurations without full Spring context startup.
Key validation checkpoints:
-
Property prefix matches between @ConfigurationProperties and test properties
-
Validation triggers on @Validated classes with invalid values
-
Type conversions work for Duration, DataSize, collections, and maps
When to Use
-
Testing @ConfigurationProperties property binding
-
Testing property name mapping and type conversions
-
Validating configuration with @NotBlank , @Min , @Max , @Email constraints
-
Testing environment-specific configurations (dev, prod)
-
Testing nested property structures and collections
-
Verifying default values when properties are not specified
-
Fast configuration tests without Spring context startup
Instructions
-
Set up test dependencies: Add spring-boot-starter-test and AssertJ dependencies
-
Use ApplicationContextRunner: Test property bindings without starting full Spring context
-
Define property prefixes: Ensure @ConfigurationProperties(prefix = "...") matches test property paths
-
Test all property paths: Verify each property including nested structures and collections
-
Test validation constraints: Use context.hasFailed() to verify @Validated properties reject invalid values
-
Test type conversions: Verify Duration (30s ), DataSize (50MB ), collections, and maps convert correctly
-
Test default values: Verify properties have correct defaults when not specified in test properties
-
Test profile-specific configs: Use @Profile with ApplicationContextRunner for environment-specific configurations
-
Test edge cases: Include empty strings, null values, and type mismatches
Troubleshooting flow:
-
If properties don't bind → Check prefix matches (kebab-case to camelCase conversion)
-
If validation doesn't trigger → Verify @Validated annotation is present
-
If context fails to start → Check dependencies and @ConfigurationProperties class structure
Examples
Setup: Test Dependencies
<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>
Basic Pattern: Property Binding
@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(); }); } }
Validation Testing
@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"); }); } }
Type Conversion Testing
@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"); }); } }
For nested properties, profile-specific configurations, collection binding, and advanced validation patterns, see references/advanced-examples.md .
Best Practices
-
Test all property bindings including nested structures and collections
-
Test validation constraints for all @NotBlank , @Min , @Max , @Email , @Positive annotations
-
Test both default and custom values to verify fallback behavior
-
Use ApplicationContextRunner for fast context-free testing
-
Test profile-specific configurations separately with @Profile
-
Verify type conversions for Duration, DataSize, collections, and maps
-
Test edge cases: empty strings, null values, type mismatches, out-of-range values
Constraints and Warnings
-
Kebab-case to camelCase: Property app.my-property maps to myProperty in Java
-
Loose binding: Spring Boot uses loose binding by default; use strict binding if needed
-
@Validated required: Add @Validated annotation to enable constraint validation
-
@ConstructorBinding : All parameters must be bindable when using constructor binding
-
List indexing: Use [0] , [1] notation; ensure sequential indexing for lists
-
Duration format: Accepts ISO-8601 (PT30S ) or simple syntax (30s , 1m , 2h )
-
Context isolation: Each ApplicationContextRunner creates a new context with no shared state
-
Profile activation: Use spring.profiles.active=profileName in withPropertyValues() for profile tests
Troubleshooting
Issue Cause Solution
Properties not binding Prefix mismatch Verify @ConfigurationProperties(prefix="...") matches property paths
Validation not triggered Missing @Validated
Add @Validated annotation to configuration class
Context fails to start Missing dependencies Ensure spring-boot-starter-test is in test scope
Nested properties null Inner class missing Use @Data on nested classes or provide getters/setters
Collection binding fails Wrong indexing Use [0] , [1] notation, not (0) , (1)