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) |
1---2name: unit-test-config-properties3description: Provides patterns for unit testing `@ConfigurationProperties` classes with `@ConfigurationPropertiesTest`. Validates property binding, tests validation constraints, verifies default values, checks type conversions, and mocks property sources for Spring Boot configuration properties. Use when testing application configuration binding, validating YAML or application.properties files, verifying environment-specific settings, or testing nested property structures.4---5
6# Unit Testing Configuration Properties and Profiles
7
8## Overview
9
10This 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.
11
12**Key validation checkpoints:**
13- Property prefix matches between `@ConfigurationProperties` and test properties
14- Validation triggers on `@Validated` classes with invalid values
15- Type conversions work for Duration, DataSize, collections, and maps
16
17## When to Use
18
19- Testing `@ConfigurationProperties` property binding
20- Testing property name mapping and type conversions
21- Validating configuration with `@NotBlank`, `@Min`, `@Max`, `@Email` constraints
22- Testing environment-specific configurations (dev, prod)
23- Testing nested property structures and collections
24- Verifying default values when properties are not specified
25- Fast configuration tests without Spring context startup
26
27## Instructions
28
291. **Set up test dependencies**: Add `spring-boot-starter-test` and AssertJ dependencies
302. **Use ApplicationContextRunner**: Test property bindings without starting full Spring context
313. **Define property prefixes**: Ensure `@ConfigurationProperties(prefix = "...")` matches test property paths
324. **Test all property paths**: Verify each property including nested structures and collections
335. **Test validation constraints**: Use `context.hasFailed()` to verify `@Validated` properties reject invalid values
346. **Test type conversions**: Verify Duration (`30s`), DataSize (`50MB`), collections, and maps convert correctly
357. **Test default values**: Verify properties have correct defaults when not specified in test properties
368. **Test profile-specific configs**: Use `@Profile` with `ApplicationContextRunner` for environment-specific configurations
379. **Test edge cases**: Include empty strings, null values, and type mismatches
38
39**Troubleshooting flow:**
40- If properties don't bind → Check prefix matches (kebab-case to camelCase conversion)
41- If validation doesn't trigger → Verify `@Validated` annotation is present
42- If context fails to start → Check dependencies and `@ConfigurationProperties` class structure
43
44## Examples
45
46### Setup: Test Dependencies
47
48```xml
49<dependency>
50 <groupId>org.springframework.boot</groupId>
51 <artifactId>spring-boot-configuration-processor</artifactId>
52 <scope>provided</scope>
53</dependency>
54<dependency>
55 <groupId>org.springframework.boot</groupId>
56 <artifactId>spring-boot-starter-test</artifactId>
57 <scope>test</scope>
58</dependency>
59<dependency>
60 <groupId>org.assertj</groupId>
61 <artifactId>assertj-core</artifactId>
62 <scope>test</scope>
63</dependency>
64```
65
66### Basic Pattern: Property Binding
67
68```java
69@ConfigurationProperties(prefix = "app.security")
70@Data
71public class SecurityProperties {
72 private String jwtSecret;
73 private long jwtExpirationMs;
74 private int maxLoginAttempts;
75 private boolean enableTwoFactor;
76}
77
78class SecurityPropertiesTest {
79
80 @Test
81 void shouldBindPropertiesFromEnvironment() {
82 new ApplicationContextRunner()
83 .withPropertyValues(
84 "app.security.jwtSecret=my-secret-key",
85 "app.security.jwtExpirationMs=3600000",
86 "app.security.maxLoginAttempts=5",
87 "app.security.enableTwoFactor=true"
88 )
89 .withBean(SecurityProperties.class)
90 .run(context -> {
91 SecurityProperties props = context.getBean(SecurityProperties.class);
92 assertThat(props.getJwtSecret()).isEqualTo("my-secret-key");
93 assertThat(props.getJwtExpirationMs()).isEqualTo(3600000L);
94 assertThat(props.getMaxLoginAttempts()).isEqualTo(5);
95 assertThat(props.isEnableTwoFactor()).isTrue();
96 });
97 }
98
99 @Test
100 void shouldUseDefaultValuesWhenPropertiesNotProvided() {
101 new ApplicationContextRunner()
102 .withPropertyValues("app.security.jwtSecret=key")
103 .withBean(SecurityProperties.class)
104 .run(context -> {
105 SecurityProperties props = context.getBean(SecurityProperties.class);
106 assertThat(props.getJwtSecret()).isEqualTo("key");
107 assertThat(props.getMaxLoginAttempts()).isZero();
108 });
109 }
110}
111```
112
113### Validation Testing
114
115```java
116@ConfigurationProperties(prefix = "app.server")
117@Data
118@Validated
119public class ServerProperties {
120 @NotBlank
121 private String host;
122
123 @Min(1)
124 @Max(65535)
125 private int port = 8080;
126
127 @Positive
128 private int threadPoolSize;
129}
130
131class ConfigurationValidationTest {
132
133 @Test
134 void shouldFailValidationWhenHostIsBlank() {
135 new ApplicationContextRunner()
136 .withPropertyValues(
137 "app.server.host=",
138 "app.server.port=8080",
139 "app.server.threadPoolSize=10"
140 )
141 .withBean(ServerProperties.class)
142 .run(context -> {
143 assertThat(context).hasFailed()
144 .getFailure()
145 .hasMessageContaining("host");
146 });
147 }
148
149 @Test
150 void shouldPassValidationWithValidConfiguration() {
151 new ApplicationContextRunner()
152 .withPropertyValues(
153 "app.server.host=localhost",
154 "app.server.port=8080",
155 "app.server.threadPoolSize=10"
156 )
157 .withBean(ServerProperties.class)
158 .run(context -> {
159 assertThat(context).hasNotFailed();
160 assertThat(context.getBean(ServerProperties.class).getHost()).isEqualTo("localhost");
161 });
162 }
163}
164```
165
166### Type Conversion Testing
167
168```java
169@ConfigurationProperties(prefix = "app.features")
170@Data
171public class FeatureProperties {
172 private Duration cacheExpiry = Duration.ofMinutes(10);
173 private DataSize maxUploadSize = DataSize.ofMegabytes(100);
174 private List<String> enabledFeatures;
175 private Map<String, String> featureFlags;
176}
177
178class TypeConversionTest {
179
180 @Test
181 void shouldConvertDurationFromString() {
182 new ApplicationContextRunner()
183 .withPropertyValues("app.features.cacheExpiry=30s")
184 .withBean(FeatureProperties.class)
185 .run(context -> {
186 assertThat(context.getBean(FeatureProperties.class).getCacheExpiry())
187 .isEqualTo(Duration.ofSeconds(30));
188 });
189 }
190
191 @Test
192 void shouldConvertCommaDelimitedList() {
193 new ApplicationContextRunner()
194 .withPropertyValues("app.features.enabledFeatures=feature1,feature2")
195 .withBean(FeatureProperties.class)
196 .run(context -> {
197 assertThat(context.getBean(FeatureProperties.class).getEnabledFeatures())
198 .containsExactly("feature1", "feature2");
199 });
200 }
201}
202```
203
204For **nested properties**, **profile-specific configurations**, **collection binding**, and **advanced validation patterns**, see `references/advanced-examples.md`.
205
206## Best Practices
207
208- **Test all property bindings** including nested structures and collections
209- **Test validation constraints** for all `@NotBlank`, `@Min`, `@Max`, `@Email`, `@Positive` annotations
210- **Test both default and custom values** to verify fallback behavior
211- **Use ApplicationContextRunner** for fast context-free testing
212- **Test profile-specific configurations** separately with `@Profile`
213- **Verify type conversions** for Duration, DataSize, collections, and maps
214- **Test edge cases**: empty strings, null values, type mismatches, out-of-range values
215
216## Constraints and Warnings
217
218- **Kebab-case to camelCase**: Property `app.my-property` maps to `myProperty` in Java
219- **Loose binding**: Spring Boot uses loose binding by default; use strict binding if needed
220- **`@Validated` required**: Add `@Validated` annotation to enable constraint validation
221- **`@ConstructorBinding`**: All parameters must be bindable when using constructor binding
222- **List indexing**: Use `[0]`, `[1]` notation; ensure sequential indexing for lists
223- **Duration format**: Accepts ISO-8601 (`PT30S`) or simple syntax (`30s`, `1m`, `2h`)
224- **Context isolation**: Each `ApplicationContextRunner` creates a new context with no shared state
225- **Profile activation**: Use `spring.profiles.active=profileName` in `withPropertyValues()` for profile tests
226
227## Troubleshooting
228
229| Issue | Cause | Solution |
230|-------|-------|----------|
231| Properties not binding | Prefix mismatch | Verify `@ConfigurationProperties(prefix="...")` matches property paths |
232| Validation not triggered | Missing `@Validated` | Add `@Validated` annotation to configuration class |
233| Context fails to start | Missing dependencies | Ensure `spring-boot-starter-test` is in test scope |
234| Nested properties null | Inner class missing | Use `@Data` on nested classes or provide getters/setters |
235| Collection binding fails | Wrong indexing | Use `[0]`, `[1]` notation, not `(0)`, `(1)` |