テストでの Bean のオーバーライド

テストでの Bean オーバーライドとは、テストクラスまたはテストクラス内の 1 つ以上の非静的フィールドにアノテーションを付けることで、テストクラスの ApplicationContext 内の特定の Bean をオーバーライドする機能を指します。

この機能は、DefaultListableBeanFactory setAllowBeanDefinitionOverriding フラグを true に設定して @Bean 経由で Bean を登録する方法よりもリスクの少ない代替手段として意図されています。

Spring TestContext フレームワークは、Bean オーバーライド用のアノテーションを 2 セット提供します。

前者は純粋に Spring に依存し、後者のセットは Mockito (英語) サードパーティライブラリに依存します。

カスタム Bean オーバーライドサポート

上記の 3 つのアノテーションは、@BeanOverride メタアノテーションと関連インフラストラクチャに基づいて構築されており、カスタム Bean オーバーライドバリアントを定義できます。

カスタム Bean オーバーライドサポートを実装するには、次のものが必要です。

  • 使用する BeanOverrideProcessor を定義する @BeanOverride でアノテーションされたアノテーションメタ

  • カスタム BeanOverrideProcessor 実装

  • プロセッサーによって作成された 1 つ以上の具体的な BeanOverrideHandler 実装

Spring TestContext フレームワークには、Bean オーバーライドをサポートし、インフラストラクチャの残りの部分を設定する次の API の実装が含まれています。

  • BeanFactoryPostProcessor

  • ContextCustomizerFactory

  • TestExecutionListener

spring-test モジュールは、後者の 2 つ (BeanOverrideContextCustomizerFactory と BeanOverrideTestExecutionListener) の実装を META-INF/spring.factories プロパティファイル [GitHub] (英語) に登録します。

Bean オーバーライドインフラストラクチャは、テストクラスのアノテーションと、@BeanOverride でメタアノテーションされたテストクラス内の非静的フィールドのアノテーションを検索し、適切な BeanOverrideHandler を作成するための対応する BeanOverrideProcessor をインスタンス化します。

次に、内部の BeanOverrideBeanFactoryPostProcessor は Bean オーバーライドハンドラーを使用して、対応する BeanOverrideStrategy で定義されているように Bean を作成、置換、ラップすることにより、テストの ApplicationContext を変更します。

REPLACE

Bean を置き換えます。対応する Bean が存在しない場合は例外をスローします。

REPLACE_OR_CREATE

Bean が存在する場合は置き換えます。対応する Bean が存在しない場合は、新しい Bean を作成します。

WRAP

オリジナルの Bean を取り出し、折り返しします。

非シングルトン Bean を置き換える場合、非シングルトン Bean は、該当する BeanOverrideHandler によって作成された Bean オーバーライドインスタンスに対応するシングルトン Bean に置き換えられ、対応する Bean 定義は singleton に変換されます。ハンドラーが prototype またはスコープ付き Bean をオーバーライドした場合、オーバーライドされた Bean は singleton として扱われます。

FactoryBean によって作成された Bean を置き換える場合、FactoryBean 自体は、該当する BeanOverrideHandler によって作成された Bean オーバーライドインスタンスに対応するシングルトン Bean に置き換えられます。

FactoryBean によって作成された Bean をラップする場合、FactoryBean 自体ではなく、FactoryBean によって作成されたオブジェクトがラップされます。

Spring のオートワイヤーメカニズム (たとえば、@Autowired フィールドの解決) とは対照的に、TestContext フレームワークの Bean オーバーライドインフラストラクチャでは、Bean を見つけるために実行できるヒューリスティックが限られています。BeanOverrideProcessor はオーバーライドする Bean の名前を計算するか、アノテーション付きフィールドの型とその修飾アノテーションに基づいて明確に選択することができます。

通常、Bean は BeanOverrideFactoryPostProcessor によって「型別」に選択されます。あるいは、ユーザーはカスタムアノテーションで Bean 名を直接指定することもできます。

BeanOverrideProcessor 実装では、規則またはその他の方法に基づいて Bean 名を内部的に計算する場合もあります。

Bean オーバーライドと Spring AOP プロキシ

Spring ApplicationContext 内の Bean は、@Transactional@Cacheable@Retryable のセマンティクスをサポートするために、AOP プロキシでラップされることがよくあります。オーバーライドされた Bean がそのようなプロキシを保持するかどうかは、オーバーライドを作成するために使用された BeanOverrideStrategy に依存します。

  • REPLACE または REPLACE_OR_CREATE 戦略を使用するオーバーライド(@TestBean や @MockitoBean など)は、オーバーライドインスタンスを手動シングルトンとして直接登録するため、コンテナーの通常の Bean 後処理がバイパスされます。その結果、オーバーライドインスタンスはベアオブジェクトとなり、元の Bean に適用されるはずの AOP アドバイス(@Transactional@Cacheable@Retryable、メソッドセキュリティなど)は一切適用されません。

  • WRAP 戦略を使用するオーバーライド(@MockitoSpyBean など)は、コンテナーの残りのポストプロセッサー(AOP プロキシの作成を担当するものを含む)が実行される前に、元の Bean への早期参照を取得し、それを使用してオーバーライドインスタンスを作成します。元の Bean がプロキシ化されていた場合、そのプロキシは作成されますが、元の Bean ではなくオーバーライドインスタンスをラップします。ApplicationContext に最終的に含まれ、連携する Bean やテストクラスに注入される Bean は、オーバーライドインスタンスをターゲットとする AOP プロキシであり、オーバーライドインスタンスそのものではありません。

以下の図は、注入された Bean のメソッドを呼び出す呼び出し元の視点から見た、各戦略における Bean の最終的な形状を示しています。

REPLACE または REPLACE_OR_CREATE 戦略では、AOP プロキシは一切存在せず、呼び出し元はオーバーライドインスタンスを直接呼び出します。

caller
  │
  ▼
[ override instance ]

WRAP 戦略では、通常であれば元の Bean をラップするはずだった AOP プロキシは引き続き作成されますが、代わりにオーバーライドインスタンスをラップするようになります。

caller
  │
  ▼
[ AOP proxy ]          (for example, retry, caching, or transaction advice)
  │
  │  delegates to its target
  ▼
[ override instance ]  (for example, a Mockito spy created by @MockitoSpyBean)

@MockitoSpyBean のような WRAP ベースのオーバーライドの場合、AOP プロキシによって実行される「折り返し」は、結果として生成される Mockito スパイ自体が、作成元の Bean インスタンスを「ラップ」する方法とは無関連です。上記のプロキシは、呼び出し元が実際にどのオブジェクトを呼び出すかを決定しますが、スパイと元のインスタンスとの関連は、スタブ化されていないメソッドがスパイ上で呼び出されたときに何が起こるかのみを決定します。つまり、そのインスタンスの実際の動作にフォールスルーします。

この違いは、Bean のオーバーライドを Mockito のスタブ化および検証 API と組み合わせる際に、実際的な影響を及ぼします。詳細は @MockitoSpyBean および Spring AOP プロキシを参照してください。