こんにちは、仙台オフィスのはんだです。
今回は、MIRAILのAndroidアプリのユニットテスト実装中につまずいたことがあったので、その際に調べた内容を紹介したいと思います。
MockKについて
MIRAILのAndroidアプリでは、ユニットテストで使用するモックの実装にMockKを利用しています。
app/build.gradle
にdependencyを追加するとインストールできます。このやり方以外のインストール方法は、上の公式ページを確認ください。
testImplementation "io.mockk:mockk:${mockkVersion}"
なお、以下はV1.13.4で確認した内容です。
MockKについて調べた経緯
例えば、検索条件に合うタイトルを取得するリポジトリのモックを簡単に書くと、以下のような感じです。
val mockRepository = mockk<TestRepository> { // fetchTitlesが呼ばれたら無条件で "title1", "title2", "title3"の入った配列を返す coEvery { fetchTitles(parameter = any()) } returns listOf( "title1", "title2", "title3", ) }
上のfetchTitles
のparameter
にはこのようなクラスが入るとします。
data class RequestParam( val word: String, // 検索ワード val limit: Int, // 取得するタイトルの数 )
上記の例では、テスト対象からmockRepository.fetchTitles()
が呼ばれると、無条件でタイトルの入った配列が返ってきます。
ただ、上のモックではfetchTitles
の引数の部分がany()
になっているため引数の値が考慮されず、仮にRequestParam
のlimit
が0だった場合でも同じようにタイトルが3つ返ってきてしまいます。モックなので、それはそれでいいとも考えられるとは思いますが、違和感があったので何とかできないかと思い調べてみました。
つまづいた点
引数に入るものが単純にInt型であれば、MockKで用意されているeq()
やmore()
のようなMatcher
を使って判定できます。
// 引数がInt型ならこんな感じで使える coEvery { fetchTitles(parameter = more(0)) }
ただ、今回の例のようにカスタムクラスを判定したい場合は使えません。
また、以下のように引数の値を直接指定することもできなくはないのですが、これだと本来は考慮する必要がないword
の値も指定しなくてはならず、現実的ではないと思います。
coEvery { fetchTitles(parameter = RequestParam( word = "word", limit = 0, )) returns ...
解決策
色々と試したところ、limitの値が0以下なら空の配列を返すようにするには、以下のようにすればうまくいくようでした。
解決策1 : answers
を使う
先ほどからの例で returns
としている箇所でanswers
を使うと、ブロック内でさまざまな操作ができます。これを利用して、以下のようにブロック内でlimit
の値を判定することができます。
coEvery { fetchTitles(any()) } answers { if (firstArg<RequestParam>().limit > 0) { // firstArg<T>()で、最初の引数の値を取得できる // limitが0より大きければタイトルが入った配列を返す listOf( "title1", "title2", "title3", ) } else { // limitが0以下なら空の配列を返す emptyList() } }
ただし、firstArg<RequestParam>
のようなやり方で値を取得しなければならないのと、ブロック内で何でもできてしまうのとで、あまり安全ではない気がします。
解決策2 : match
を使う
MockKでmatcher
として用意されているmatch
を使うと、以下のように実装できるようでした。
coEvery { // limitが0より大きければタイトルが入った配列を返す fetchTitles(parameter = match { it.limit > 0 }) } returns listOf( "title1", "title2", "title3", ) coEvery { // limitが0以下なら空の配列を返す fetchTitles(parameter = match { it.limit <= 0 }) } returns emptyList()
match
に続くブロック内ではit
で引数を取得することができるので直感的ですし、先ほどのanswers
を使った例よりも扱いやすいかと思います。
あとがき
以上、MockKを使ったモックについて調べた内容を紹介しました。
弊社のアプリチーム内で同じ内容を共有した際に、「テストやモックがあまりに複雑になると、テストのためのテストが必要になる〜」という話をもらいました。確かにその通りだと思うので、本記事のようにモック内で引数を判定するのは必ずしも正しいとは言えないかもしれませんが、このようなやり方もあると知っておくと役に立つ機会があるかもと思い紹介させていただきました。
今回は以上です。お読みいただきありがとうございました。