unit-test-config-properties — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited unit-test-config-properties (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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:
@ConfigurationProperties and test properties@Validated classes with invalid values@ConfigurationProperties property binding@NotBlank, @Min, @Max, @Email constraintsspring-boot-starter-test and AssertJ dependencies@ConfigurationProperties(prefix = "...") matches test property pathscontext.hasFailed() to verify @Validated properties reject invalid values30s), DataSize (50MB), collections, and maps convert correctly@Profile with ApplicationContextRunner for environment-specific configurationsTroubleshooting flow:
@Validated annotation is present@ConfigurationProperties class structure<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>@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();
});
}
}@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");
});
}
}@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.
@NotBlank, @Min, @Max, @Email, @Positive annotations@Profileapp.my-property maps to myProperty in Java@Validated annotation to enable constraint validation[0], [1] notation; ensure sequential indexing for listsPT30S) or simple syntax (30s, 1m, 2h)ApplicationContextRunner creates a new context with no shared statespring.profiles.active=profileName in withPropertyValues() for profile tests| 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) |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.