unit-test-config-properties

Test Spring @ConfigurationProperties bindings, validation, and type conversions without full context startup. Use ApplicationContextRunner to test property binding in isolation, covering simple properties, nested structures, collections, and type conversions Verify validation constraints with @Validated annotations, ensuring invalid values fail appropriately and valid configurations pass Test default values, profile-specific configurations, and property name mapping (kebab-case to camelCase conversion) Covers Duration, DataSize, List, Map, and Charset type conversions with practical examples for each

INSTALLATION
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-config-properties
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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)

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card