unit-test-config-properties

Unit Testing Configuration Properties and Profiles

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "unit-test-config-properties" with this command: npx skills add giuseppe-trisciuoglio/developer-kit/giuseppe-trisciuoglio-developer-kit-unit-test-config-properties

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. It covers testing property name mapping, type conversions, validation constraints, nested structures, and profile-specific configurations without full Spring context startup.

When to Use

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

Instructions

  • Use ApplicationContextRunner: Test property bindings without starting full Spring context

  • Test all property paths: Verify each property including nested structures and collections

  • Test validation constraints: Ensure @Validated properties fail with invalid values

  • Test type conversions: Verify Duration, DataSize, and other special types convert correctly

  • Test default values: Verify properties have correct defaults when not specified

  • Test profile-specific configs: Use @Profile to test environment-specific configurations

  • Verify property prefixes: Ensure the prefix in @ConfigurationProperties matches test properties

  • Test edge cases: Include empty strings, null values, and type mismatches

Examples

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", "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

@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

Constraints and Warnings

  • Property name matching: Kebab-case in properties (app.my-prop) maps to camelCase in Java (myProp)

  • Loose binding by default: Spring Boot supports loose binding; enable strict binding if needed

  • Validation requires @Validated: Add @Validated to enable validation on configuration properties

  • @ConstructorBinding limitations: When using @ConstructorBinding, all parameters must be bindable

  • List indexing: List properties use [0], [1] notation; ensure sequential indexing

  • Duration format: Duration properties accept standard ISO-8601 format or simple syntax (10s, 1m)

  • ApplicationContextRunner isolation: Each ApplicationContextRunner creates a new context; there's no shared state

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

  • ApplicationContextRunner Testing

  • Spring Profiles

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

shadcn-ui

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

tailwind-css-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

unit-test-bean-validation

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

react-patterns

No summary provided by upstream source.

Repository SourceNeeds Review