--- name: unit-test-config-properties description: Unit tests for @ConfigurationProperties classes with @ConfigurationPropertiesTest. Use when validating application configuration binding and validation. category: testing tags: [junit-5, configuration-properties, spring-profiles, property-binding] version: 1.0.1 --- # Unit Testing Configuration Properties and Profiles Test @ConfigurationProperties bindings, environment-specific configurations, and property validation using JUnit 5. Verify configuration loading without full Spring context startup. ## When to Use This Skill 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 ## Setup: Configuration Testing ### Maven ```xml org.springframework.boot spring-boot-configuration-processor provided org.springframework.boot spring-boot-starter-test test org.junit.jupiter junit-jupiter test org.assertj assertj-core test ``` ### Gradle ```kotlin 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 ```java // 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 ```java @ConfigurationProperties(prefix = "app.database") @Data public class DatabaseProperties { private String url; private String username; private Pool pool = new Pool(); private List 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 ```java @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 ```java @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 ```java @ConfigurationProperties(prefix = "app.features") @Data public class FeatureProperties { private Duration cacheExpiry = Duration.ofMinutes(10); private DataSize maxUploadSize = DataSize.ofMegabytes(100); private List enabledFeatures; private Map 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 ```java @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 ## 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](https://docs.spring.io/spring-boot/docs/current/reference/html/configuration-metadata.html) - [ApplicationContextRunner Testing](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/context/runner/ApplicationContextRunner.html) - [Spring Profiles](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.profiles)