最新の安定バージョンについては、Spring Modulith 2.0.0 を使用してください! |
統合テストアプリケーションモジュール
Spring Modulith を使用すると、個々のアプリケーションモジュールを単独で、または他のモジュールと組み合わせてブートストラップして統合テストを実行できます。これを実現するには、次のように Spring Modulith テストスターターをプロジェクトに追加します。
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<scope>test</scope>
</dependency>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 をモックする方が良い選択肢です。
Java
Kotlin
@ApplicationModuleTest
class InventoryIntegrationTests {
@MockBean SomeOtherComponent someOtherComponent;
}
@ApplicationModuleTest
class InventoryIntegrationTests {
@MockBean SomeOtherComponent someOtherComponent
}
Spring Boot は、@MockBean として定義された型の Bean 定義とインスタンスを作成し、テスト実行用にブートストラップされた ApplicationContext に追加します。
アプリケーションモジュールが他のモジュールの Bean にあまりにも多く依存していることに気付いた場合、通常、それらの Bean 間の結合度が高いことを示しています。依存関係を再検討し、ドメインイベントの公開による置き換えの候補になるかどうかを検討する必要があります。
統合テストのシナリオの定義
アプリケーションモジュールの統合テストは、非常に手の込んだ作業になる場合があります。特に、これらの統合が非同期のトランザクションイベント処理に基づいている場合、同時実行の処理では微妙なエラーが発生する可能性があります。また、イベントが発行されてトランザクションリスナーに配信されることを確認するための TransactionOperations と ApplicationEventProcessor、同時実行性を処理するための Awaitility、テスト実行の結果についての期待を定式化するための AssertJ アサーションなど、かなりの数のインフラストラクチャコンポーネントを処理する必要があります。
アプリケーションモジュール統合テストの定義を容易にするために、Spring Modulith は、@ApplicationModuleTest として宣言されたテストでテストメソッドパラメーターとして宣言して使用できる Scenario 抽象化を提供します。
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
}
}
テスト定義自体は通常、次の骨子に従います。
システムへの stimulus が定義されています。これは通常、イベントのパブリケーション、モジュールによって公開される Spring コンポーネントの呼び出しのいずれかです。
実行の技術的詳細のオプションのカスタマイズ (タイムアウトなど)
何らかの予想される結果の定義。たとえば、公開されたコンポーネントを呼び出すことで検出できる、何らかの条件に一致する別のアプリケーションイベントの発生やモジュールの状態変化など。
オプションで、受信したイベントまたは観測された変更された状態に対して行われる追加の検証。
Scenario は、これらの手順を定義し、定義をガイドする API を公開します。
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(Runnable { 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.java)
.matching { event -> … }
.toArriveAndVerify { event -> … }
予想される完了シグナルとして機能するイベントパブリケーションの代わりに、公開されているコンポーネントの 1 つでメソッドを呼び出すことによって、アプリケーションモジュールの状態をインスペクションすることもできます。シナリオは次のようになります。
Java
Kotlin
scenario.publish(new MyApplicationEvent(…))
.andWaitForStateChange(() -> someBean.someMethod(…)))
.andVerify(result -> …);
scenario.publish(MyApplicationEvent(…))
.andWaitForStateChange { someBean.someMethod(…) }
.andVerify { result -> … }
… .andVerify(…) メソッドに渡される result は、状態変化を検出するためのメソッド呼び出しによって返される値になります。デフォルトでは、非 null 値と空ではない Optional は決定的な状態変化とみなされます。これは、 … .andWaitForStateChange(… , Predicate) オーバーロードを使用して調整できます。
シナリオ実行のカスタマイズ
個々のシナリオの実行をカスタマイズするには、Scenario のセットアップチェーンで … .customize(…) メソッドを呼び出します。
Scenario 実行のカスタマイズ Java
Kotlin
scenario.publish(new MyApplicationEvent(…))
.customize(conditionFactory -> conditionFactory.atMost(Duration.ofSeconds(2)))
.andWaitForEventOfType(SomeOtherEvent.class)
.matching(event -> …)
.toArriveAndVerify(event -> …);
scenario.publish(MyApplicationEvent(…))
.customize { it.atMost(Duration.ofSeconds(2)) }
.andWaitForEventOfType(SomeOtherEvent::class.java)
.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 conditionFactory -> …;
}
}
}
@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): UnaryOperator<ConditionFactory> {
return UnaryOperator { conditionFactory -> … }
}
}
}