SpringBoot3、kotlin、mockkでのテスト
SpringBoot3、kotlin、mockk、OpenAPIでサーバサイドアプリを書いています。テストケースを書いている中で色々躓き等ありましたので、ご披露したいと思います。
SpringBootだと素のkotlinとはコンポーネントのライフサイクルの管理が異なるので注意が必要です。
Testクラスのクラスアノテーション
SpringBootのテストクラスの書き始めは以下の様になります
@SpringBootTest
@ExtendWith(MockKExtension::class)
class MyRepositoryTest {
}
モック化の方法
モック化の指定は以下の様になります。
/** テスト対象 */
@SpykBean
lateinit var MyRepository: myRepository
MyRepositoryが更にMySubRepositoryをインジェクションしている場合は以下の様にすると、SpringがMySubRepositoryをモック化し、それをMyRepositoryに自動的にインジェクションしてくれます。(MyRepositoryがMySubRepositoryをインジェクションしていれば)
/** テスト対象 */
@SpykBean
lateinit var MyRepository: myRepository
/** モック対象 */
@MockkBean
lateinit var MySubRepository: mySubRepository
Springを使う場合とSpringを使わない素のkotlinの場合の違いがここです。MockKのアノテーション
@Spyk
lateinit var MyRepository: myRepository
@Mockk
lateinit var MySubRepository: mySubRepository
この場合、MyRepository、MySubRepositoryのインスタンスはSpringのインスタンスのライフサイクル管理とは切り離されてしまいます。
MyRepositoryの中でMySubRepositoryをインジェクションしていても、インスタンスを自動的にインジェクションしてくれません。
の違いは、xxxBeanが付くとSpringのインスタンス管理、付かないと素のkotlin管理。SpyXXXは一部のメソッドをモック化、MockXXXは全体をモック化の違いとなります。
テスト対象クラスの一部のメソッドをテストし、一部のメソッドをモック化したい場合
テスト対象クラスの中でテスト対象メソッドが、同じクラスのメソッドを呼び出していている場合がよくあると思います。この場合、@SpykBeanを使えばよいのですが、
@SpringBootTest
@ExtendWith(MockKExtension::class)
class MyRepositoryTest {
@Spyk
lateinit var MyRepository: myRepository
@Test
fun mytest() {
// モック化
every { myRepository.mySubFunc1(any()) } returns Unit
// 実行
val result = myRepository.myFunc()
}
}
myFuncの中からmySubFunc1を呼び出しているという前提です。
で、mySubFunc1をモック化して戻り値を設定、myFuncは期待通りの結果となりテストは成功します。
しかし、これが、複数のテストメソッドを実行すると失敗する場合があります。何故なら、以下のような場合
@SpringBootTest
@ExtendWith(MockKExtension::class)
class MyRepositoryTest {
@Spyk
lateinit var MyRepository: myRepository
@Test
fun mytest1() {
// モック化
every { myRepository.mySubFunc1(any()) } returns Unit
// 実行
val result = myRepository.myFunc()
}
@Test
fun mytest2() {
// モック化
every { myRepository.mySubFunc2(any()) } returns Unit
// 実行
val result = myRepository.myFunc()
}
@Test
fun mytest2() {
// モック化
every { myRepository.mySubFunc3(any()) } returns Unit
// 実行
val result = myRepository.myFunc()
}
}
lateinit var MyRepository: myRepository
のインスタンスは1個なので、テストメソッドの中でメソッドがモック化される度にモック化されたメソッドが積み重なっていきます。つまり、
mytest1、mytest2、mytest3の順でテストが実行されとすると、
- mytest1では、mySubFunc1がモック化
- mytest2では、mySubFunc1、mySubFunc2がモック化
- mytest3では、mySubFunc1、mySubFunc2、mySubFunc3がモック化
となるため、意図しない結果となります。
これを回避するためにはテストメソッドの都度、Spyをリセットする必要があります。
@SpringBootTest
@ExtendWith(MockKExtension::class)
class MyRepositoryTest {
@AfterEach
fun tearDown() {
clearAllMocks() // ★
}
@Spyk
lateinit var MyRepository: myRepository
@Test
fun mytest1() {
// モック化
every { myRepository.mySubFunc1(any()) } returns Unit
// 実行
val result = myRepository.myFunc()
}
@Test
fun mytest2() {
// モック化
every { myRepository.mySubFunc2(any()) } returns Unit
// 実行
val result = myRepository.myFunc()
}
@Test
fun mytest2() {
// モック化
every { myRepository.mySubFunc3(any()) } returns Unit
// 実行
val result = myRepository.myFunc()
}
}
@AfterEachでテストメソッドの終了の都度、clearAllMocks()でspyをリセットすることで、期待通りの結果となります。