【Android・MockK】ユニットテストのモックから引数に応じた値を返す

こんにちは、仙台オフィスのはんだです。

今回は、MIRAILのAndroidアプリのユニットテスト実装中につまずいたことがあったので、その際に調べた内容を紹介したいと思います。

MockKについて

MIRAILのAndroidアプリでは、ユニットテストで使用するモックの実装にMockKを利用しています。

mockk.io

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",
    )
}

上のfetchTitlesparameterにはこのようなクラスが入るとします。

data class RequestParam(
    val word: String,    // 検索ワード
    val limit: Int,    // 取得するタイトルの数
)

上記の例では、テスト対象からmockRepository.fetchTitles()が呼ばれると、無条件でタイトルの入った配列が返ってきます。

ただ、上のモックではfetchTitlesの引数の部分がany()になっているため引数の値が考慮されず、仮にRequestParamlimitが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を使ったモックについて調べた内容を紹介しました。

弊社のアプリチーム内で同じ内容を共有した際に、「テストやモックがあまりに複雑になると、テストのためのテストが必要になる〜」という話をもらいました。確かにその通りだと思うので、本記事のようにモック内で引数を判定するのは必ずしも正しいとは言えないかもしれませんが、このようなやり方もあると知っておくと役に立つ機会があるかもと思い紹介させていただきました。

今回は以上です。お読みいただきありがとうございました。