@MockitoBean および @MockitoSpyBean

@MockitoBean (Javadoc) と @MockitoSpyBean (Javadoc) は、テストクラスで使用して、テストの ApplicationContext の Bean をそれぞれ Mockito mock または spy でオーバーライドできます。後者の場合、元の Bean の初期インスタンスがスパイによってキャプチャーされ、ラップされます。

アノテーションは次の方法で適用できます。

  • テストクラスまたはそのスーパークラスの非静的フィールド。

  • @Nested テストクラスの包含クラス内の非静的フィールド、または @Nested テストクラスの上位の型階層または包含クラス階層内の任意のクラス。

  • テストクラスの型レベル、またはテストクラスの上位の型階層内の任意のスーパークラスまたは実装されたインターフェース。

  • @Nested テストクラスの包含クラスの型レベル、または @Nested テストクラスの上位の型階層または包含クラス階層内の任意のクラスまたはインターフェース。

フィールドで @MockitoBean または @MockitoSpyBean が宣言されている場合、モックまたはスパイする Bean は、アノテーションが付けられたフィールドの型から推論されます。ApplicationContext に複数の候補が存在する場合は、曖昧さを解消するために、フィールドに @Qualifier アノテーションを宣言できます。@Qualifier アノテーションがない場合、アノテーションが付けられたフィールドの名前がフォールバック修飾子として使用されます。または、アノテーションで value または name 属性を設定することで、モックまたはスパイする Bean 名を明示的に指定することもできます。

@MockitoBean または @MockitoSpyBean が型レベルで宣言されている場合、モックまたはスパイする Bean (または Bean) の型は、アノテーションの types 属性を介して指定する必要があります (例: @MockitoBean(types = {OrderService.class, UserService.class}))。ApplicationContext に複数の候補が存在する場合は、name 属性を設定することで、モックまたはスパイする Bean 名を明示的に指定できます。ただし、明示的な Bean name が構成されている場合 (例: @MockitoBean(name = "ps1", types = PrintingService.class))、types 属性には単一の型が含まれている必要があることに注意してください。

モック構成の再利用をサポートするために、@MockitoBean と @MockitoSpyBean をメタアノテーションとして使用して、カスタム合成アノテーションを作成できます。たとえば、テストスイート全体で再利用できる単一のアノテーションで共通のモック構成またはスパイ構成を定義できます。@MockitoBean と @MockitoSpyBean は、型レベルで繰り返し可能なアノテーションとして使用することもできます。たとえば、名前で複数の Bean をモックまたはスパイできます。

フィールド名などの修飾子は、別の ApplicationContext を作成する必要があるかどうかを判断するために使用されます。この機能を使用して、複数のテストクラスで同じ Bean をモックまたはスパイする場合は、不要なコンテキストを作成しないように、フィールドに一貫した名前を付けるようにしてください。

@MockitoBean または @MockitoSpyBean を @ContextHierarchy と組み合わせて使用すると、各 @MockitoBean または @MockitoSpyBean がデフォルトですべてのコンテキスト階層レベルに適用されるため、望ましくない結果が生じる可能性があります。特定の @MockitoBean または @MockitoSpyBean が単一のコンテキスト階層レベルに適用されるようにするには、contextName 属性を、設定されている @ContextConfiguration 名(例: @MockitoBean(contextName = "app-config") または @MockitoSpyBean(contextName = "app-config"))と一致するように設定してください。

詳細と例については、Bean オーバーライドによるコンテキスト階層を参照してください。

各アノテーションは、モック動作を微調整するための Mockito 固有の属性も定義します。

@MockitoBean アノテーションは、Bean のオーバーライドに REPLACE_OR_CREATE 戦略を使用します。対応する Bean が存在しない場合は、新しい Bean が作成されます。ただし、enforceOverride 属性を true (たとえば @MockitoBean(enforceOverride = true)) に設定することで、REPLACE 戦略に切り替えることができます。この戦略では、コンテナーの通常の Bean 後処理をバイパスして Bean を直接置き換えるため、結果として生成されるモックはベアオブジェクトになります。元の Bean が @Transactional、@Cacheable、@Retryable によってラップされる場合でも、モックは Spring AOP プロキシでラップされることはありません。詳細については、Bean オーバーライドと Spring AOP プロキシを参照してください。

@MockitoSpyBean アノテーションは WRAP  戦略を使用します。つまり、元の Bean の初期インスタンスがキャプチャーされ、それを使用して Mockito スパイが作成されます。この戦略では、候補となる Bean が正確に 1 つだけ存在する必要があります。@MockitoBean とは対照的に、元の Bean が Spring AOP プロキシでラップされていた場合、そのプロキシは引き続き作成されますが、元の Bean の代わりにスパイをラップします。スタブ化と検証への影響に関する図と詳細については、@MockitoSpyBean および Spring AOP プロキシを参照してください。

Mockito のドキュメントに記載されているように、スパイのスタブに Mockito.when() を使用することが不適切な場合があります。たとえば、スパイで実際のメソッドを呼び出すと、望ましくない副作用が発生する場合などです。

このような望ましくない副作用を避けるには、Mockito.doReturn(…​).when(spy)…​、Mockito.doThrow(…​).when(spy)…​、Mockito.doNothing().when(spy)…​ や同様の方法の使用を検討してください。

@MockitoBean を使用して非シングルトン Bean をモックすると、非シングルトン Bean はシングルトンモックに置き換えられ、対応する Bean の定義は singleton に変換されます。prototype またはスコープ付き Bean をモックすると、そのモックは singleton として扱われます。

同様に、@MockitoSpyBean を使用して非シングルトン Bean のスパイを作成すると、対応する Bean 定義は singleton に変換されます。prototype またはスコープ付き Bean のスパイを作成すると、そのスパイは singleton として扱われます。

@MockitoBean を使用して FactoryBean によって作成された Bean をモックする場合、FactoryBean は FactoryBean によって作成されたオブジェクトの型のシングルトンモックに置き換えられます。

同様に、@MockitoSpyBean を使用して FactoryBean のスパイを作成すると、FactoryBean 自体ではなく、FactoryBean によって作成されたオブジェクトのスパイが作成されます。

さらに、@MockitoSpyBean はスコープ付きプロキシ(たとえば、@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) でアノテーションされた Bean)をスパイするために使用することはできません。そのような試みはすべて例外で失敗します。

@MockitoBean フィールドと @MockitoSpyBean フィールドの可視性には制限はありません。

このようなフィールドは、プロジェクトのニーズやコーディング方法に応じて、public、protected、パッケージプライベート (デフォルトの可視性)、または private になります。

@MockitoBean の例

次の例は、@MockitoBean アノテーションのデフォルトの動作を使用する方法を示しています。

  • Java

  • Kotlin

@SpringJUnitConfig(TestConfig.class)
class BeanOverrideTests {

	@MockitoBean (1)
	CustomService customService;

	// tests...
}
1Mockito モックを使用して、Bean を型 CustomService に置き換えます。
@SpringJUnitConfig(TestConfig::class)
class BeanOverrideTests {

	@MockitoBean (1)
	lateinit var customService: CustomService

	// tests...
}
1Mockito モックを使用して、Bean を型 CustomService に置き換えます。

上記の例では、CustomService のモックを作成しています。その型の Bean が複数存在する場合は、customService という名前の Bean が考慮されます。そうでない場合、テストは失敗し、オーバーライドする CustomService Bean を識別するために何らかの修飾子を指定する必要があります。そのような Bean が存在しない場合は、自動生成された Bean 名で Bean が作成されます。

次の例では、型による検索ではなく、名前による検索を使用します。service という名前の Bean が存在しない場合は、作成されます。

  • Java

  • Kotlin

@SpringJUnitConfig(TestConfig.class)
class BeanOverrideTests {

	@MockitoBean("service") (1)
	CustomService customService;

	// tests...

}
1service という名前の Bean を Mockito モックに置き換えます。
@SpringJUnitConfig(TestConfig::class)
class BeanOverrideTests {

	@MockitoBean("service") (1)
	lateinit var customService: CustomService

	// tests...

}
1service という名前の Bean を Mockito モックに置き換えます。

次の @SharedMocks アノテーションは、型によるモック 2 つと名前によるモック 1 つを登録します。

  • Java

  • Kotlin

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@MockitoBean(types = {OrderService.class, UserService.class}) (1)
@MockitoBean(name = "ps1", types = PrintingService.class) (2)
public @interface SharedMocks {
}
1OrderService および UserService モックを型別に登録します。
2PrintingService のモック名を登録します。
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MockitoBean(types = [OrderService::class, UserService::class]) (1)
@MockitoBean(name = "ps1", types = [PrintingService::class]) (2)
annotation class SharedMocks
1OrderService および UserService モックを型別に登録します。
2PrintingService のモック名を登録します。

以下は、@SharedMocks をテストクラスで使用する方法を示しています。

  • Java

  • Kotlin

@SpringJUnitConfig(TestConfig.class)
@SharedMocks (1)
class BeanOverrideTests {

	@Autowired OrderService orderService; (2)

	@Autowired UserService userService; (2)

	@Autowired PrintingService ps1; (2)

	// Inject other components that rely on the mocks.

	@Test
	void testThatDependsOnMocks() {
		// ...
	}
}
1 カスタム @SharedMocks アノテーションを介して共通モックを登録します。
2 必要に応じて、モックを挿入してスタブ化または検証します。
@SpringJUnitConfig(TestConfig::class)
@SharedMocks (1)
class BeanOverrideTests {

	@Autowired
	lateinit var orderService: OrderService (2)

	@Autowired
	lateinit var userService: UserService (2)

	@Autowired
	lateinit var ps1: PrintingService (2)

	// Inject other components that rely on the mocks.

	@Test
	fun testThatDependsOnMocks() {
		// ...
	}
}
1 カスタム @SharedMocks アノテーションを介して共通モックを登録します。
2 必要に応じて、モックを挿入してスタブ化または検証します。
モックは、@Configuration クラスまたは ApplicationContext 内のその他のテスト関連コンポーネントに挿入して、Mockito のスタブ API を使用して構成することもできます。

@MockitoSpyBean の例

次の例は、@MockitoSpyBean アノテーションのデフォルトの動作を使用する方法を示しています。

  • Java

  • Kotlin

@SpringJUnitConfig(TestConfig.class)
class BeanOverrideTests {

	@MockitoSpyBean (1)
	CustomService customService;

	// tests...
}
1Bean を型 CustomService で Mockito スパイでラップします。
@SpringJUnitConfig(TestConfig::class)
class BeanOverrideTests {

	@MockitoSpyBean (1)
	lateinit var customService: CustomService

	// tests...
}
1Bean を型 CustomService で Mockito スパイでラップします。

上記の例では、Bean を型 CustomService でラップしています。その型の Bean が複数存在する場合は、customService という名前の Bean が考慮されます。そうでない場合、テストは失敗し、スパイする CustomService Bean を識別するために何らかの修飾子を提供する必要があります。

次の例では、型による検索ではなく、名前による検索を使用します。

  • Java

  • Kotlin

@SpringJUnitConfig(TestConfig.class)
class BeanOverrideTests {

	@MockitoSpyBean("service") (1)
	CustomService customService;

	// tests...
}
1service という名前の Bean を Mockito スパイでラップします。
@SpringJUnitConfig(TestConfig::class)
class BeanOverrideTests {

	@MockitoSpyBean("service") (1)
	lateinit var customService: CustomService

	// tests...
}
1service という名前の Bean を Mockito スパイでラップします。

次の @SharedSpies アノテーションは、型別に 2 つのスパイと名前別に 1 つのスパイを登録します。

  • Java

  • Kotlin

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@MockitoSpyBean(types = {OrderService.class, UserService.class}) (1)
@MockitoSpyBean(name = "ps1", types = PrintingService.class) (2)
public @interface SharedSpies {
}
1OrderService および UserService スパイを型別に登録します。
2PrintingService スパイの名前を登録します。
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MockitoSpyBean(types = [OrderService::class, UserService::class]) (1)
@MockitoSpyBean(name = "ps1", types = [PrintingService::class]) (2)
annotation class SharedSpies
1OrderService および UserService スパイを型別に登録します。
2PrintingService スパイの名前を登録します。

以下は、@SharedSpies をテストクラスで使用する方法を示しています。

  • Java

  • Kotlin

@SpringJUnitConfig(TestConfig.class)
@SharedSpies (1)
class BeanOverrideTests {

	@Autowired OrderService orderService; (2)

	@Autowired UserService userService; (2)

	@Autowired PrintingService ps1; (2)

	// Inject other components that rely on the spies.

	@Test
	void testThatDependsOnMocks() {
		// ...
	}
}
1 カスタム @SharedSpies アノテーションを介して共通スパイを登録します。
2 オプションでスパイを挿入してスタブ化または検証します。
@SpringJUnitConfig(TestConfig::class)
@SharedSpies (1)
class BeanOverrideTests {

	@Autowired
	lateinit var orderService: OrderService (2)

	@Autowired
	lateinit var userService: UserService (2)

	@Autowired
	lateinit var ps1: PrintingService (2)

	// Inject other components that rely on the spies.

	@Test
	fun testThatDependsOnMocks() {
		// ...
	}
}
1 カスタム @SharedSpies アノテーションを介して共通スパイを登録します。
2 オプションでスパイを挿入してスタブ化または検証します。
スパイは、@Configuration クラスまたは ApplicationContext 内のその他のテスト関連コンポーネントに挿入して、Mockito のスタブ API を使用して構成することもできます。

@MockitoSpyBean および Spring AOP プロキシ

Bean オーバーライドと Spring AOP プロキシに従って、スパイ対象の Bean が通常 Spring AOP プロキシでラップされている場合(たとえば、@Transactional、@Cacheable、@Retryable による場合)、そのプロキシは引き続き作成され、スパイがそのターゲットとなります。テストクラスおよび ApplicationContext 内の他の Bean に注入される Bean は、スパイ自体ではなく、プロキシです。

Mockito の verify() API による検証は、この影響を受けず、プロキシ上で呼び出された場合でも、基盤となるスパイ上で呼び出された場合でも、透過的に動作します。

プロキシ経由のスタブ

スタブ化は検証よりも注意が必要です。なぜなら、Mockito.doReturn(…​).when(…​)、Mockito.doThrow(…​).when(…​)、同様のメソッドは、プロキシ上で呼び出される際に関係する AOP アドバイスの性質に応じて動作が異なるためです。

when は Kotlin の予約語であるため、以下の Kotlin の例では、Mockito.doReturn(…​).when(…​) と Mockito.doThrow(…​).when(…​) の代わりに BDDMockito の given(…​)、willReturn(…​)、willThrow(…​) メソッドを使用します。

@Retryable のように、呼び出し間で状態を保持しないアドバイスは、スタブ処理に悪影響を与えません。プロキシ上で呼び出される以下のスタブ処理シーケンスは、スローされた例外が発生した場合に再試行をトリガーするなど、基盤となるスパイに直接実行した場合とまったく同じように動作します。

  • Java

  • Kotlin

doReturn("ok")
	.doThrow(new RuntimeException("Message delivery failed"))
	.doReturn("ok again")
	.when(clientService).sendMessage(any()); (1)
1clientService は注入されたプロキシです。@Retryable のアドバイスはステートレスなパススルーであるため、例外をスローする呼び出しも含め、すべての呼び出しはスパイに直接到達します。
willReturn("ok")
	.willThrow(RuntimeException("Message delivery failed"))
	.willReturn("ok again")
	.given(clientService).sendMessage(any()) (1)
1clientService は注入されたプロキシです。@Retryable のアドバイスはステートレスなパススルーであるため、例外をスローする呼び出しも含め、すべての呼び出しはスパイに直接到達します。

@Cacheable のように、呼び出しの結果をキャッシュしたりメモ化したりするアドバイスは、同じようには動作しません。doReturn(…​)、doThrow(…​)、同様の宣言が記録されている間、Mockito はスパイの実際の動作や以前にスタブ化された動作を呼び出しません。代わりに、スタブ化を宣言するために使用された呼び出しは空の値を返します (たとえば、null)。その呼び出しがプロキシで行われた場合、キャッシュアドバイスはこの空の値にキャッシュし、その結果、スタブ化を構成するはずだった呼び出しを含め、その引数の組み合わせに対してスパイが永続的にシャドウされます。

  • Java

  • Kotlin

doReturn(1L).when(dateService).getDate(false); (1)
dateService.getDate(false); (2)
1dateService は注入されたプロキシです。この呼び出しはスパイに到達する前に Mockito のスタブインフラストラクチャによってインターセプトされるため、キャッシュアドバイスは引数 false に対して空の値をキャッシュすることになります。
2 前回の呼び出しでキャッシュされた空の値(1L ではない)を返します。これは、キャッシュがすでにデータで満たされているためです。
willReturn(1L).given(dateService).getDate(false) (1)
dateService.getDate(false) (2)
1dateService は注入されたプロキシです。この呼び出しはスパイに到達する前に Mockito のスタブインフラストラクチャによってインターセプトされるため、キャッシュアドバイスは引数 false に対して空の値をキャッシュすることになります。
2 前回の呼び出しでキャッシュされた空の値(1L ではない)を返します。これは、キャッシュがすでにデータで満たされているためです。

これを回避するには、プロキシではなくスパイに対して直接スタブを作成し、AopTestUtils.getUltimateTargetObject(…​) (Javadoc) を使用してプロキシをアンラップします。

  • Java

  • Kotlin

DateService spy = AopTestUtils.getUltimateTargetObject(dateService);
doReturn(1L).when(spy).getDate(false);
val spy = AopTestUtils.getUltimateTargetObject<DateService>(dateService)
willReturn(1L).given(spy).getDate(false)

テストに対する AOP アドバイスの無効化

上記のようにプロキシを回避するのではなく、テスト中は基となる AOP アドバイスを無効にし、本番コードには @Retryable、@Cacheable、同様のアノテーションを残しておくという方法もあります。一般的な理由としては、テストスイートの実行速度を低下させる再試行の遅延を回避すること、あるいはキャッシュを完全に回避してすべての呼び出しがスパイに直接到達するようにすることなどが挙げられます。後者の場合、プロキシをアンラップする必要もなく、前述のスタブ化の落とし穴を回避できます。

一般的な手法としては、アドバイスの実効動作を制御する要素(たとえば、再試行回数や @Cacheable を支える CacheManager など)を外部化し、テスト時のみその設定をオーバーライドします。通常は、Bean オーバーライドやテスト固有のプロパティを使用します。プロキシとそのアドバイスは作成されますが、テスト中はそれらの動作は単に何もしない、あるいはそのまま通過するだけになります。

@Retryable の場合、maxRetriesString 属性を適切なデフォルト値を持つプロパティプレースホルダーにバインドし (プロパティが設定されていない場合でも本番環境の設定に影響がないように)、テスト内でそのプロパティを @TestPropertySource で上書きして、再試行が行われないようにします。

  • Java

  • Kotlin

@Retryable(maxRetriesString = "${sendMessage.maxRetries:3}", delay = 10)
public String sendMessage(String request) {
	// ...
}
@Retryable(maxRetriesString = "\${sendMessage.maxRetries:3}", delay = 10)
fun sendMessage(request: String): String {
	// ...
}
  • Java

  • Kotlin

@SpringJUnitConfig
@TestPropertySource(properties = "sendMessage.maxRetries = 0") (1)
class ClientServiceTests {

	@MockitoSpyBean
	ClientService clientService;

	// test case body...
}
1 再試行が許可されていないため、最初の(そして唯一の)試行が行われ、スローされた例外が即座に伝播するため、スパイのスタブチェーンは、doThrow(…​) 応答の場合も含め、宣言どおりに動作します。
@SpringJUnitConfig
@TestPropertySource(properties = ["sendMessage.maxRetries = 0"]) (1)
class ClientServiceTests {

	@MockitoSpyBean
	lateinit var clientService: ClientService

	// test case body...
}
1 再試行が許可されていないため、最初の(そして唯一の)試行が行われ、スローされた例外が即座に伝播するため、スパイのスタブチェーンは、doThrow(…​) 応答の場合も含め、宣言どおりに動作します。

@Cacheable の場合、Spring は NoOpCacheManager (Javadoc) を提供します。これは、キャッシュエントリを受け入れるものの、実際には保存しない CacheManager です。そのため、呼び出しのたびにキャッシュミスが発生し、結果としてターゲットメソッドが呼び出されます。CacheManager Bean を NoOpCacheManager (たとえば @TestBean を使用) でオーバーライドすると、本番コードの @Cacheable アノテーションを変更することなく、テストのキャッシュを効果的に無効にできます。

  • Java

  • Kotlin

@SpringJUnitConfig
class DateServiceTests {

	@MockitoSpyBean
	DateService dateService;

	@TestBean (1)
	CacheManager cacheManager;

	static CacheManager cacheManager() { (2)
		return new NoOpCacheManager();
	}

	@Test
	void test() {
		doReturn(1L).when(dateService).getDate(false);
		assertThat(dateService.getDate(false)).isEqualTo(1L);

		doReturn(2L).when(dateService).getDate(false);
		assertThat(dateService.getDate(false)).isEqualTo(2L); (3)
	}
}
1 このテストでは、CacheManager Bean を上書きしてください。
2@Cacheable が実際には何もキャッシュしないように、NoOpCacheManager に置き換えてください。
3 もはや古いキャッシュエントリによって隠蔽されることはなく、すべての呼び出しがスパイに到達します。
@SpringJUnitConfig
class DateServiceTests {

	@MockitoSpyBean
	lateinit var dateService: DateService

	@TestBean (1)
	lateinit var cacheManager: CacheManager

	companion object {
		@JvmStatic
		fun cacheManager(): CacheManager { (2)
			return NoOpCacheManager()
		}
	}

	@Test
	fun test() {
		willReturn(1L).given(dateService).getDate(false)
		assertThat(dateService.getDate(false)).isEqualTo(1L)

		willReturn(2L).given(dateService).getDate(false)
		assertThat(dateService.getDate(false)).isEqualTo(2L) (3)
	}
}
1 このテストでは、CacheManager Bean を上書きしてください。
2@Cacheable が実際には何もキャッシュしないように、NoOpCacheManager に置き換えてください。
3 もはや古いキャッシュエントリによって隠蔽されることはなく、すべての呼び出しがスパイに到達します。