unit-test-config-properties
Install this skill
npx skills add giuseppe-trisciuoglio/developer-kitWorks across Claude Code, Cursor, Codex, Copilot & Antigravity
This skill focuses on validating Spring Boot @ConfigurationProperties without the overhead of booting a full application context. By employing the ApplicationContextRunner, developers can isolate property binding logic, ensuring that environment variables, custom prefixes, and complex nested objects map correctly to Java beans. This approach significantly accelerates the feedback loop during development, as tests execute rapidly without initializing database connections, network resources, or heavy beans associated with standard Spring integration tests. It specifically targets type conversion accuracy, default value verification, and list or map population within configuration classes, ensuring that externalized configuration is reliably ingested before the application starts.
When to Use This Skill
- •Validating data class mappings for application-specific YAML configuration
- •Testing custom property type converters before production deployment
- •Ensuring default fallback values work when properties are missing
- •Verifying complex nested structures like database pools or service lists
How to Invoke This Skill
Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:
- “How to test spring boot properties without loading full context
- “unit test for configurationproperties
- “verify configuration properties binding using applicationcontextrunner
- “test nested spring configuration objects quickly
- “how to validate property value mapping in spring boot
Pro Tips
- 💡Always prioritize `@ConfigurationPropertiesTest` for focused configuration validation over `@SpringBootTest` to ensure maximum test speed and isolation.
- 💡Leverage Spring's `@Validated` annotation and custom validators within your `@ConfigurationProperties` classes to enforce complex business rules, testing them thoroughly with this skill.
- 💡When simulating different property values for testing, effectively use `TestPropertyValues` to inject specific configurations directly into your test context.
What this skill does
- •Verify @ConfigurationProperties binding with precise property values
- •Validate type conversion for custom configuration structures
- •Test default value assignment and fallback logic
- •Assert nested object hierarchies and list collection mappings
- •Simulate specific environment profiles in isolation
When not to use it
- ✕When you need to test actual interaction with external infrastructure like databases or APIs
- ✕When you must verify complex dependency injection or bean post-processor behavior across the full application context
Example workflow
- Define a POJO annotated with @ConfigurationProperties
- Create a JUnit 5 test class using ApplicationContextRunner
- Inject specific properties using .withPropertyValues()
- Assert that the bean fields match the injected values
- Verify behavior under both populated and default scenarios
Prerequisites
- –spring-boot-starter-test
- –spring-boot-configuration-processor
- –assertj-core
Pitfalls & limitations
- !Failure to handle case-insensitive property mapping may lead to confusing test results
- !Forgetting to register the bean under test within the context runner will cause binding errors
- !Cannot detect syntax errors in external YAML files, only the resulting Java object state
FAQ
How it compares
While manual tests or full @SpringBootTest methods often result in long wait times due to context startup, this approach isolates the specific binding logic for sub-second execution speeds.
📄 Full skill instructions — original source: giuseppe-trisciuoglio/developer-kit
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
<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",
"[email protected]"
)
.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
## 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)
How to Use This Skill Unit
Option A: Project-Specific (Recommended)
- Click "Download" above
- In your project, create the directory:
.agent/skills/unit-test-config-properties/ - Save the file as
SKILL.md - The agent will automatically discover the skill based on its description.
Option B: Global Installation (All Agents)
Save the file to these locations to make it available across all projects:
- Claude Code:
~/.claude/skills/giuseppe-trisciuoglio/developer-kit/unit-test-config-properties/SKILL.md - Cursor:
~/.cursor/skills/giuseppe-trisciuoglio/developer-kit/unit-test-config-properties/SKILL.md - Antigravity:
~/.gemini/antigravity/skills/giuseppe-trisciuoglio/developer-kit/unit-test-config-properties/SKILL.md
🚀 Install with CLI:npx skills add giuseppe-trisciuoglio/developer-kit