最新の安定バージョンについては、Spring Modulith 1.2.1 を使用してください!

統合テストアプリケーションモジュール

Spring Modulith を使用すると、個々のアプリケーションモジュールを単独で、または他のモジュールと組み合わせてブートストラップする統合テストを実行できます。これを実現するには、JUnit テストクラスをアプリケーションモジュールパッケージまたはそのサブパッケージに配置し、それに @ApplicationModuleTest アノテーションを付けます。

アプリケーションモジュール統合テストクラス
  • Java

  • Kotlin

package example.order;

@ApplicationModuleTest
class OrderIntegrationTests {

  // Individual test cases go here
}
package example.order

@ApplicationModuleTest
class OrderIntegrationTests {

  // Individual test cases go here
}

これにより、@SpringBootTest が達成するものと同様の統合テストが実行されますが、ブートストラップは実際にはテストが存在するアプリケーションモジュールに限定されます。org.springframework.modulith から DEBUG のログレベルを構成すると、テスト実行のカスタマイズ方法に関する詳細情報が表示されます。Spring Boot ブートストラップ:

アプリケーションモジュール統合テストブートストラップのログ出力
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::       (v3.0.0-SNAPSHOT)

… - Bootstrapping @ApplicationModuleTest for example.order in mode STANDALONE (class example.Application)…
… - ======================================================================================================
… - ## example.order ##
… - > Logical name: order
… - > Base package: example.order
… - > Direct module dependencies: none
… - > Spring beans:
… -       + ….OrderManagement
… -       + ….internal.OrderInternal
… - Starting OrderIntegrationTests using Java 17.0.3 …
… - No active profile set, falling back to 1 default profile: "default"
… - Re-configuring auto-configuration and entity scan packages to: example.order.

テスト実行に含まれるモジュールに関する詳細情報が出力にどのように含まれるかに注目してください。アプリケーションモジュールモジュールを作成し、実行するモジュールを見つけて、自動構成、コンポーネント、エンティティスキャンのアプリケーションを対応するパッケージに制限します。

ブートストラップモード

アプリケーションモジュールのテストは、さまざまなモードでブートストラップできます。

  • STANDALONE (default) — 現在のモジュールのみを実行します。

  • DIRECT_DEPENDENCIES — 現在のモジュールと、現在のモジュールが直接依存するすべてのモジュールを実行します。

  • ALL_DEPENDENCIES — 現在のモジュールと依存するモジュールのツリー全体を実行します。

遠心性依存関係への対処

アプリケーションモジュールがブートストラップされると、それに含まれる Spring Bean がインスタンス化されます。これらにモジュールの境界を越える Bean 参照が含まれている場合、それらの他のモジュールがテスト実行に含まれていないとブートストラップは失敗します (詳細についてはブートストラップモードを参照)。含まれるアプリケーションモジュールの範囲を拡大するのが自然な反応かもしれませんが、通常はターゲット Bean をモックする方が良い選択肢です。

他のアプリケーションモジュールでの Spring Bean 依存関係のモック化
  • Java

  • Kotlin

@ApplicationModuleTest
class InventoryIntegrationTests {

  @MockBean SomeOtherComponent someOtherComponent;
}
@ApplicationModuleTest
class InventoryIntegrationTests {

  @MockBean SomeOtherComponent someOtherComponent
}

Spring Boot は、@MockBean として定義された型の Bean 定義とインスタンスを作成し、テスト実行用にブートストラップされた ApplicationContext に追加します。

アプリケーションモジュールが他の Bean に依存しすぎていることが判明した場合、通常、それらの間の結合が高いことを示しています。依存関係は、ドメインイベントを発行して置換の候補であるかどうかを確認する必要があります ( null を参照)。

統合テストのシナリオの定義

アプリケーションモジュールの統合テストは、非常に手の込んだ作業になる場合があります。特に、これらの統合が非同期のトランザクションイベント処理に基づいている場合、同時実行の処理では微妙なエラーが発生する可能性があります。また、イベントが発行されてトランザクションリスナーに配信されることを確認するための TransactionOperations と ApplicationEventProcessor、同時実行性を処理するための Awaitility、テスト実行の結果についての期待を定式化するための AssertJ アサーションなど、かなりの数のインフラストラクチャコンポーネントを処理する必要があります。

アプリケーションモジュール統合テストの定義を容易にするために、Spring Modulith は、@ApplicationModuleTest として宣言されたテストのテストメソッドパラメーターとして宣言することで使用できる Scenario 抽象化を提供します。

JUnit 5 テストでの Scenario API の使用
  • Java

  • Kotlin

@ApplicationModuleTest
class SomeApplicationModuleTest {

  @Test
  public void someModuleIntegrationTest(Scenario scenario) {
    // Use the Scenario API to define your integration test
  }
}
@ApplicationModuleTest
class SomeApplicationModuleTest {

  @Test
  fun someModuleIntegrationTest(scenario: Scenario) {
    // Use the Scenario API to define your integration test
  }
}

テスト定義自体は通常、次の骨子に従います。

  1. システムへの stimulus が定義されています。これは通常、イベントのパブリケーション、モジュールによって公開される Spring コンポーネントの呼び出しのいずれかです。

  2. 実行の技術的詳細のオプションのカスタマイズ (タイムアウトなど)

  3. 何らかの予想される結果の定義。たとえば、公開されたコンポーネントを呼び出すことで検出できる、何らかの条件に一致する別のアプリケーションイベントの発生やモジュールの状態変化など。

  4. オプションで、受信したイベントまたは観察された変更された状態に対して行われる追加の検証。

Scenario は、これらのステップを定義し、定義をガイドするための API を公開します。

stimulus を Scenario の開始点として定義する
  • Java

  • Kotlin

// Start with an event publication
scenario.publish(new MyApplicationEvent(…)).…

// Start with a bean invocation
scenario.stimulate(() -> someBean.someMethod(…)).…
// Start with an event publication
scenario.publish(MyApplicationEvent(…)).…

// Start with a bean invocation
scenario.stimulate(() -> someBean.someMethod(…)).…

イベントの発行と Bean の呼び出しは両方ともトランザクションコールバック内で発生し、指定されたイベントまたは Bean の呼び出し中に発行されたイベントがトランザクションイベントリスナーに確実に配信されます。これには、テストケースがトランザクション内ですでに実行されているかどうかに関係なく、新しいトランザクションを開始する必要があることに注意してください。つまり、stimulus によってトリガーされたデータベースの状態変更は決してロールバックされず、手動でクリーンアップする必要があります。その目的については、 … .andCleanup(…) メソッドを参照してください。

結果として得られるオブジェクトは、汎用の  … .customize(…) メソッド、またはタイムアウトの設定 (… .waitAtMost(…)) などの一般的なユースケースに特化したメソッドを通じてカスタマイズされた実行を取得できるようになりました。

セットアップフェーズは、stimulus の結果の実際の期待値を定義することによって終了します。これは特定の型のイベントになる可能性があり、必要に応じてマッチャーによってさらに制約されます。

操作結果としてイベントが公開されることを期待する
  • Java

  • Kotlin

….andWaitForEventOfType(SomeOtherEvent.class)
 .matching(event -> …) // Use some predicate here
 .…
….andWaitForEventOfType(SomeOtherEvent.class)
 .matching(event -> …) // Use some predicate here
 .…

これらの行は、最終的な実行が続行するまで待機する完了条件を設定します。つまり、上記の例では、デフォルトのタイムアウトに達するか、定義された述語に一致する SomeOtherEvent が発行されるまで、最終的に実行がブロックされます。

イベントベースの Scenario を実行するターミナル操作は  … .toArrive … () と呼ばれ、必要に応じて、発行される予期されるイベント、または元の stimulus で定義された Bean 呼び出しの結果オブジェクトにアクセスできます。

検証のトリガー
  • Java

  • Kotlin

// Executes the scenario
….toArrive(…)

// Execute and define assertions on the event received
….toArriveAndVerify(event -> …)
// Executes the scenario
….toArrive(…)

// Execute and define assertions on the event received
….toArriveAndVerify(event -> …)

メソッド名の選択は、ステップを個別に見ると少し奇妙に見えるかもしれませんが、実際に組み合わせると非常にスムーズに読めます。

完全な Scenario 定義
  • Java

  • Kotlin

scenario.publish(new MyApplicationEvent(…))
  .andWaitForEventOfType(SomeOtherEvent.class)
  .matching(event -> …)
  .toArriveAndVerify(event -> …);
scenario.publish(new MyApplicationEvent(…))
  .andWaitForEventOfType(SomeOtherEvent::class)
  .matching(event -> …)
  .toArriveAndVerify(event -> …)

予想される完了信号として機能するイベントパブリケーションの代わりに、公開されているコンポーネントの 1 つでメソッドを呼び出すことによって、アプリケーションモジュールの状態をインスペクションすることもできます。シナリオは次のようになります。

状態の変化を期待する
  • Java

  • Kotlin

scenario.publish(new MyApplicationEvent(…))
  .andWaitForStateChange(() -> someBean.someMethod(…)))
  .andVerify(result -> …);
scenario.publish(new MyApplicationEvent(…))
  .andWaitForStateChange(() -> someBean.someMethod(…)))
  .andVerify(result -> …)

… .andVerify(…) メソッドに渡される result は、状態変化を検出するためのメソッド呼び出しによって返される値になります。デフォルトでは、非 null 値と空ではない Optional は決定的な状態変化とみなされます。これは、 … .andWaitForStateChange(… , Predicate) オーバーロードを使用して調整できます。

シナリオ実行のカスタマイズ

個々のシナリオの実行をカスタマイズするには、Scenario のセットアップチェーン で  … .customize(…) メソッドを呼び出します。

Scenario 実行のカスタマイズ
  • Java

  • Kotlin

scenario.publish(new MyApplicationEvent(…))
  .customize(it -> it.atMost(Duration.ofSeconds(2)))
  .andWaitForEventOfType(SomeOtherEvent.class)
  .matching(event -> …)
  .toArriveAndVerify(event -> …);
scenario.publish(MyApplicationEvent(…))
  .customize(it -> it.atMost(Duration.ofSeconds(2)))
  .andWaitForEventOfType(SomeOtherEvent::class)
  .matching(event -> …)
  .toArriveAndVerify(event -> …)

テストクラスのすべての Scenario インスタンスをグローバルにカスタマイズするには、ScenarioCustomizer を実装し、JUnit 拡張機能として登録します。

ScenarioCustomizer の登録
  • Java

  • Kotlin

@ExtendWith(MyCustomizer.class)
class MyTests {

  @Test
  void myTestCase(Scenario scenario) {
    // scenario will be pre-customized with logic defined in MyCustomizer
  }

  static class MyCustomizer implements ScenarioCustomizer {

    @Override
    Function<ConditionFactory, ConditionFactory> getDefaultCustomizer(Method method, ApplicationContext context) {
      return it -> …;
    }
  }
}
@ExtendWith(MyCustomizer::class)
class MyTests {

  @Test
  fun myTestCase(scenario : Scenario) {
    // scenario will be pre-customized with logic defined in MyCustomizer
  }

  class MyCustomizer : ScenarioCustomizer {

    override fun getDefaultCustomizer(method : Method, context : ApplicationContext) : Function<ConditionFactory, ConditionFactory> {
      return it -> …
    }
  }
}