🍟

Mockitoで複数のmethodのモックを作るときの注意

2022/01/16に公開

使用しているJUnitはJUnit4です。

したいこと

例えば、CatRepositoryというclassがNyaoDataSourceというclassに依存しているとします。
この時に

CatRepository.kt
@RunWith(MockitoJUnitRunner::class)
class CatRepository @Inject constructor(
    val nyaoDataSource: NyaoDataSource
): CatRepository {
    // 指定の猫が鳴けるかどうかを確認する
    override suspend fun getChirp(baseCat: BaseCat): CatInfo {
        if (nyaoDataSource.checkChirp(baseCat.nameCat) == true) {
            return nyaoDataSource.getChirp(baseCat.nameCat)
        }
        throw IllegalStateException("This cannot chirp...sorry.")
    }
}

このように書けるわけですが、このmethodをテストしようとするとnyaoDataSource.checkChirpnyaoDataSource.getChirpの2つをモックにしてテストしなくてはいけません。
したがって、今回したいことは「Mockitoで複数のmethodのモックを適切に作ること」です。

最初に作ったコード(予期せぬ動きをしたコード)

まず、予期しない動きをしたコードです。

CatRepositoryTest.kt
...中略
    private val mockNyaoLocal = mock(NyaoDataSource::class.java)
    @Test
    fun getAuthInfoTest() {
        runBlocking {
            val inputDummy = BaseCat(
                nameCat = "Tama",
                cate = "mike",
            )
            mockNyaoLocal.apply {
                `when`(checkChirp(inputDummy.nameCat)).thenReturn(false)
                `when`(getAccountInfo(inputDummy.nameCat))
		    .thenThrow(IllegalArgumentException("この猫はいない。いるかどうか、チェックしてから使ってね."))
            }

            val catRepositoryImpl = CatRepositoryImpl(mockNyaoLocal)
            val e: IllegalStateException 
	        = assertThrows(IllegalStateException::class.java){
                runBlocking {
                    catRepositoryImpl.getChirp(inputDummy)
                }
            }
	    assertEquals("この猫はいないよ。",
            e.message)
        }
    }
}

この結果、通るはずのテストが通りません。なぜかと言うと、CatRepository.ktの中にあるif文の中の処理も実行されています(実際は実行されているわけではなく、その挙動は以下の通りの推定だと思います)。
ここで、おかしいのはCatRepositoryTest.ktの中にある

 mockNyaoLocal.apply {
                `when`(checkChirp(inputDummy.nameCat)).thenReturn(false)
                `when`(getAccountInfo(inputDummy.nameCat))
		    .thenThrow(IllegalArgumentException("この猫はいない。いるかどうか、チェックしてから使ってね."))
            }

この部分です。whenのどちらかの条件に一致した場合、applyの中すべてが実行されてしまうようです。MockitoのReferenceを見ようと調べたのですが、あまりいいのが見つからず、同時にapplyの挙動について調べた結果[1][2]、なかなか理解できなかったので、私の推定が合っているかどうか、どなたか、ご教授いただけると幸いです。

動いたコード

動いたコードは以下です。

CatRepositoryTest.kt
...中略
    private val mockNyaoLocal = mock(NyaoDataSource::class.java)
    @Test
    fun getAuthInfoTest() {
        runBlocking {
            val inputDummy = BaseCat(
                nameCat = "Tama",
                cate = "mike",
            )
            doReturn(false)
                .`when`(mockNyaoLocal)
                .checkChirp(inputDummy.nameCat)
            doReturn(IllegalArgumentException("この猫はいない。いるかどうか、チェックしてから使ってね."))
                .`when`(mockNyaoLocal)
                .getChirp(inputDummy.nameCat)

            val catRepositoryImpl = CatRepositoryImpl(mockNyaoLocal)
            val e: IllegalStateException 
	        = assertThrows(IllegalStateException::class.java){
                runBlocking {
                    catRepositoryImpl.getChirp(inputDummy)
                }
            }
	    assertEquals("この猫はいないよ。",
            e.message)
        }
    }
}

先程、上手く動いていないコードを

doReturn(false)
    .`when`(mockNyaoLocal)
    .checkChirp(inputDummy.nameCat)
doReturn(IllegalArgumentException("この猫はいない。いるかどうか、チェックしてから使ってね."))
    .`when`(mockNyaoLocal)
    .getChirp(inputDummy.nameCat)

と書き換えています。こうすると、独立してそれぞれのwhenが動き、予期した動きが実現出来ます。
ですが、使われないstubということで、コンパイラに怒られますので、今回の場合下のExceptionは要りません。

脚注
  1. Kotlin スコープ関数 用途まとめ / @ngsw_taro https://qiita.com/ngsw_taro/items/d29e3080d9fc8a38691e (2022-01-16閲覧) ↩︎

  2. Kotlin の let(), apply(), run(), with() を使いこなす / Masamichi Yoshii http://extra-vision.blogspot.com/2016/11/kotlin-let-apply-run-with.html ↩︎

Discussion