unit-test-config-properties について
unit-test-config-properties は、Spring の設定プロパティや環境別設定を JUnit 5 でテストするスキルです。ApplicationContextRunner を使い、フルコンテキストを起動せずにバインド、検証、型変換、ネスト構造、既定値、プロファイルを確認します。
主な機能
- 01設定プロパティのバインドとマッピングのテスト
- 02制約と不正値の検証
- 03Duration、DataSize などの変換確認
- 04ネスト構造とコレクションの検証
- 05既定値とプロファイル別設定のテスト
活用シーン
- 01@ConfigurationProperties のバインド確認
- 02不正な設定値が拒否されるかの確認
- 03Duration とコレクションの変換確認
- 04dev と prod の設定を個別にテスト
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ giuseppe-trisciuoglio/ developer-kit --skill unit-test-config-propertiesインストール後、Claude Code で/unit-test-config-propertiesを入力して読み込まれたか確認します。
同梱ファイルreferences/advanced-examples.md
詳細
ユニットテストの設定プロパティとプロファイル
概要
このスキルでは、JUnit 5 を使用して、@ConfigurationProperties のバインディング、環境固有の設定、プロパティの検証をユニットテストするためのパターンを紹介します。完全な Spring コンテキストを起動せずに、プロパティ名のマッピング、型変換、検証制約、ネストされた構造、プロファイル固有の設定をテストします。
主な検証ポイント:
@ConfigurationPropertiesとテストプロパティの間でプロパティのプレフィックスが一致している- 無効な値を設定した
@Validatedクラスで検証が実行される - Duration、DataSize、コレクション、マップの型変換が機能する
使用する場面
@ConfigurationPropertiesのプロパティバインディングをテストする- プロパティ名のマッピングと型変換をテストする
@NotBlank、@Min、@Max、@Emailの制約を使用して設定を検証する- 環境固有の設定(dev、prod)をテストする
- ネストされたプロパティ構造とコレクションをテストする
- プロパティが指定されていない場合のデフォルト値を確認する
- Spring コンテキストを起動せずに、設定をすばやくテストする
手順
- テスト依存関係を設定する:
spring-boot-starter-testと AssertJ の依存関係を追加します - ApplicationContextRunner を使用する:Spring コンテキスト全体を起動せずにプロパティバインディングをテストします
- プロパティのプレフィックスを定義する:
@ConfigurationProperties(prefix = "...")がテストプロパティのパスと一致することを確認します - すべてのプロパティパスをテストする:ネストされた構造やコレクションを含む各プロパティを検証します
- 検証制約をテストする:
context.hasFailed()を使用して、@Validatedプロパティが無効な値を拒否することを確認します - 型変換をテストする:Duration(
30s)、DataSize(50MB)、コレクション、マップが正しく変換されることを確認します - デフォルト値をテストする:テストプロパティで指定されていない場合に、プロパティが正しいデフォルト値を持つことを確認します
- プロファイル固有の設定をテストする:環境固有の設定には、
@ProfileとApplicationContextRunnerを使用します - エッジケースをテストする:空文字列、null 値、型の不一致を含めます
トラブルシューティングの流れ:
- プロパティがバインドされない場合 → プレフィックスが一致しているか確認します(kebab-case から camelCase への変換)
- 検証が実行されない場合 →
@Validatedアノテーションが付いていることを確認します - コンテキストの起動に失敗する場合 → 依存関係と
@ConfigurationPropertiesクラスの構造を確認します
例
セットアップ:テスト依存関係
<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");
});
}
}
ネストされたプロパティ、プロファイル固有の設定、コレクションのバインディング、高度な検証パターンについては、references/advanced-examples.md を参照してください。
ベストプラクティス
- ネストされた構造やコレクションを含む、すべてのプロパティバインディングをテストします
- すべての
@NotBlank、@Min、@Max、@Email、@Positiveアノテーションの検証制約をテストします - フォールバック動作を確認するため、デフォルト値とカスタム値の両方をテストします
- コンテキストを起動しない高速なテストには ApplicationContextRunner を使用します
- プロファイル固有の設定は、
@Profileを使用して個別にテストします - Duration、DataSize、コレクション、マップの型変換を確認します
- エッジケースをテストします:空文字列、null 値、型の不一致、範囲外の値
制約と警告
- ケバブケースからキャメルケースへの変換:プロパティ
app.my-propertyは、Java ではmyPropertyに対応します - 緩やかなバインディング:Spring Boot ではデフォルトで緩やかなバインディングが使用されます。必要に応じて厳密なバインディングを使用してください
@Validatedが必要:制約の検証を有効にするには、@Validatedアノテーションを追加します@ConstructorBinding:コンストラクターバインディングを使用する場合、すべてのパラメーターがバインド可能である必要があります- リストのインデックス指定:
[0]、[1]の表記を使用します。リストでは連続したインデックスを指定してください - 期間の形式:ISO-8601(
PT30S)または簡易形式(30s、1m、2h)を受け付けます - コンテキストの分離:各
ApplicationContextRunnerは、共有状態のない新しいコンテキストを作成します - プロファイルの有効化:プロファイルのテストでは、
withPropertyValues()にspring.profiles.active=profileNameを指定します
トラブルシューティング
| 問題 | 原因 | 解決策 |
|---|---|---|
| プロパティがバインドされない | プレフィックスが一致しない | @ConfigurationProperties(prefix="...") がプロパティのパスと一致していることを確認します |
| 検証が実行されない | @Validated がない | 設定クラスに @Validated アノテーションを追加します |
| コンテキストの起動に失敗する | 依存関係が不足している | spring-boot-starter-test がテストスコープに含まれていることを確認します |
| ネストされたプロパティが null になる | 内部クラスがない | ネストされたクラスで @Data を使用するか、ゲッターとセッターを用意します |
| コレクションのバインディングに失敗する | インデックスの指定が誤っている | (0)、(1) ではなく、[0]、[1] の表記を使用します |
コメント
Markdown 対応