2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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をリセットすることで、期待通りの結果となります。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?