mcpField
U
unit-test-config-properties

unit-test-config-properties

github.com/giuseppe-trisciuoglioテスト・品質3653.7k 回インストール7 週間前に更新
リポジトリ

unit-test-config-properties について

unit-test-config-properties は、Spring の設定プロパティや環境別設定を JUnit 5 でテストするスキルです。ApplicationContextRunner を使い、フルコンテキストを起動せずにバインド、検証、型変換、ネスト構造、既定値、プロファイルを確認します。

主な機能

  1. 01設定プロパティのバインドとマッピングのテスト
  2. 02制約と不正値の検証
  3. 03Duration、DataSize などの変換確認
  4. 04ネスト構造とコレクションの検証
  5. 05既定値とプロファイル別設定のテスト

活用シーン

  1. 01@ConfigurationProperties のバインド確認
  2. 02不正な設定値が拒否されるかの確認
  3. 03Duration とコレクションの変換確認
  4. 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 コンテキストを起動せずに、設定をすばやくテストする

手順

  1. テスト依存関係を設定する:spring-boot-starter-test と AssertJ の依存関係を追加します
  2. ApplicationContextRunner を使用する:Spring コンテキスト全体を起動せずにプロパティバインディングをテストします
  3. プロパティのプレフィックスを定義する:@ConfigurationProperties(prefix = "...") がテストプロパティのパスと一致することを確認します
  4. すべてのプロパティパスをテストする:ネストされた構造やコレクションを含む各プロパティを検証します
  5. 検証制約をテストする:context.hasFailed() を使用して、@Validated プロパティが無効な値を拒否することを確認します
  6. 型変換をテストする:Duration(30s)、DataSize(50MB)、コレクション、マップが正しく変換されることを確認します
  7. デフォルト値をテストする:テストプロパティで指定されていない場合に、プロパティが正しいデフォルト値を持つことを確認します
  8. プロファイル固有の設定をテストする:環境固有の設定には、@Profile と ApplicationContextRunner を使用します
  9. エッジケースをテストする:空文字列、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] の表記を使用します

コメント