【Kotlin】モックライブラリMockKでCoroutinesのユニットテストをしてみる


普段TDDまではできていませんが、ちょっとずつテストを充実させていきたい気持ちがあり、勉強がてらCoroutinesのユニットテストをしてみたときのメモです。

MockKとは

Kotlin向けモックライブラリ。今回はこれを使います。
MockK | mocking library for Kotlin

有名なMockitoのKotlin版でmockito-kotlinというライブラリもあるようですが、
MockKはCoroutinesをサポートしており、Mockitoより使いやすいという評判もある、いい感じのライブラリです
強いて言うなら参考になるような記事がMockitoに比べると少ないことがネックではありますが、公式ドキュメントがしっかりしているのであまり気にならない印象です。

実装

今回対象となるプロジェクトはMVVMにレイヤーの概念(プレゼンテーション層、ドメイン層、データ層)を加えた形で設計していたので、UseCase・ViewModelそれぞれについて簡単なテストをしてみます。
※アーキテクチャについては本題とずれるのでこれ以上の説明を省きますが、下記記事の図が参考になるかなと思います。
Android開発でMVVMを採用してみて - Qiita

今回のコードを含んだリポジトリを公開しているので、詳細はそちらをご覧ください。
https://github.com/orimomo/my-qiita-app

MockKの導入

build.gradleに下記を追加します。

build.gradle
testImplementation ‘io.mockk:mockk:1.9.3’ //追加

UseCaseのテスト

コードがこちら。

ArticleUseCaseTest.kt
class ArticleUseCaseTest {
    // Repositoryのモックインスタンスの生成
    private val mockRepository = mockk<ArticleRepository> {
        // パターンの設定
        coEvery { getArticles(any(), any()) } returns listOf(ArticleEntity(), ArticleEntity())
    }

    // モックインスタンスをuseCaseに注入
    private val useCase = ArticleUseCase(mockRepository)

    @Test
    fun getArticles() = runBlocking {
        // メソッドを呼び出してlistに格納
        val list = useCase.getArticles("555", "kotlin")

        // メソッドが正しく呼び出されたことのチェック
        coVerify { mockRepository.getArticles("555", "kotlin") }
        // 正しい結果が得られたことのチェック
        assertEquals(2, list.size)
    }
}

やっていることはコメントを入れている通りなのですが、ポイントを挙げておきます。

  • テスト対象クラスであるArticleUseCaseArticleRepositoryに依存しているので、それをMockに置き換える
  • coEveryで、生成したRepositoryのモックインスタンスにパターンを定義する
    • 引数の型を特に意識しない場合はany()を使う
  • useCase.getArticles()はsuspend functionなので、runBlockingを使う
  • coVerifyでメソッドが正しく呼ばれたことのチェックをする
  • assertEqualsで狙い通りの結果が得られたことのチェックをする

Runしてみると、無事にテストが通りました!

ViewModelのテスト

先ほどと似ているコードですが、UseCaseのテストと比べると少しだけ厄介です。

ListViewModelTest.kt
class ListViewModelTest {
    // LiveDataをテストするために必要
    @get:Rule
    val rule = InstantTaskExecutorRule()

    // UseCaseのモックインスタンスの生成
    private val mockUseCase = mockk<ArticleUseCase> {
        // パターンの設定
        coEvery { getArticles(any(), any()) } returns listOf(ArticleEntity(), ArticleEntity())
    }

    // モックインスタンスをviewModelに注入
    private val viewModel = ListViewModel(mockUseCase)

    @Test
    fun load() = runBlocking {
        // メソッドの呼び出し
        viewModel.load()

        // メソッドが正しく呼び出されたことのチェック
        coVerify { mockUseCase.getArticles(any(), any()) }
        // 正しい結果が得られたことのチェック
        assertEquals(2, viewModel.kotlinArticles.value?.size)
        assertEquals(ListViewModel.Status.COMPLETED, viewModel.status.value)
    }
}

class名の下に、UseCaseのテストにはなかった2行が追加されているのがわかるかと思います。

@get:Rule
val rule = InstantTaskExecutorRule()

これはLiveDataをテストするときに必要となるコードで、その定義をするためにはcore-testingというライブラリを別途追加する必要があります。

build.gradle
testImplementation ‘androidx.arch.core:core-testing:2.1.0’ //追加

これを定義せずにRunすると、下記エラーが出てテストが失敗します。

java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked.

改めてポイントを挙げます。

  • テスト対象クラスであるListViewModelArticleUseCaseに依存しているので、それをMockに置き換える
  • coEveryで、生成したuseCaseのモックインスタンスにパターンを定義する
  • useCase.load()はsuspend functionなので、runBlockingを使う
  • coVerifyでメソッドが正しく呼ばれたことのチェックをする
  • assertEqualsで狙い通りの結果が得られたことのチェックをする
    • viewModel.kotlinArticlesviewModel.statusはViewModel内でLiveDataとして定義されているので、それらをテストするためにはInstantTaskExecutorRuleをruleに指定する必要がある
ListViewModel
class ListViewModel(private val useCase: ArticleUseCase) : ViewModel(), LifecycleObserver {
    val kotlinArticles = MutableLiveData<List<ArticleEntity>>() //LiveDataとして定義
    val status = MutableLiveData<Status>() //LiveDataとして定義
    ...
}

Runしてみると、無事にテストが通りました!

おわりに

今回は簡単なテストをしてみましたが、今後は色々なシナリオを作って、テストを充実させていけたらと思います
手探りでやっておりますので、もし間違っている点や改善できる点などあればコメントいただければ幸いですmm

参考