Back to Testing & Quality Assurance

unit-test-config-properties

Spring Boot@ConfigurationPropertiesUnit TestingJUnit 5Configuration ValidationJavaTesting StrategiesApplication Configuration
282📄 MIT🕒 2026-06-15Source ↗

Install this skill

npx skills add giuseppe-trisciuoglio/developer-kit

Works 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

  1. Define a POJO annotated with @ConfigurationProperties
  2. Create a JUnit 5 test class using ApplicationContextRunner
  3. Inject specific properties using .withPropertyValues()
  4. Assert that the bean fields match the injected values
  5. 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

Why use ApplicationContextRunner instead of @SpringBootTest?
ApplicationContextRunner is significantly faster because it avoids loading the full Spring application context, which includes auto-configuration and component scanning.
Does this test validate my YAML file syntax?
No, this skill tests the Java configuration class, not the external YAML file itself. You would need an integration test or linting tool for file validation.
Can I test nested lists with this?
Yes, you can provide index-based properties such as app.list[0].field in the context runner to verify list binding.

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.

Source & trust

282 stars📄 MIT🕒 Updated 2026-06-15
📄 Full skill instructions — original source: giuseppe-trisciuoglio/developer-kit
# 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
<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)

  1. Click "Download" above
  2. In your project, create the directory: .agent/skills/unit-test-config-properties/
  3. Save the file as SKILL.md
  4. 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

Read the Master Guide: Mastering Agent Skills

Recommended Rules

View more rules

Recommended Workflows

View more workflows

Recommended MCP Servers

View more MCP servers

Take It Further

Maximize your productivity with these powerful resources

📋

Define Your Standards

Set up coding standards to ensure this workflow produces consistent, high-quality results.

Browse Rules Library
📖

Master Workflows

Learn how to create custom workflows, use Turbo Mode, and build your automation library.

Complete Guide

How to use this Skill in Claude Code & Cursor

For Claude Code (CLI)

To use this skill in Claude Code, copy the rule content into your project's custom instructions or follow our Add-Skill CLI guide. This ensures Claude follows your standards during every code generation.

For Cursor & Windsurf

For Cursor or Windsurf, individual skills are best used in the "Rules for AI" section. This specific unit helps the agent avoid testing & quality assurance issues, leading to cleaner, more efficient code.

Why the skill format matters: the standardized Agent Skills format lets your AI agent load detailed instructions only when they are relevant, keeping your prompt clean while improving results.

Source & attribution

This skill is categorized under Testing & Quality Assurance and is published by Giuseppe Trisciuoglio, maintained in giuseppe-trisciuoglio/developer-kit.

← Browse All Agent Skills
Sponsored AI assistant. Recommendations may be paid.