普段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に下記を追加します。
testImplementation ‘io.mockk:mockk:1.9.3’ //追加
UseCaseのテスト
コードがこちら。
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)
}
}
やっていることはコメントを入れている通りなのですが、ポイントを挙げておきます。
- テスト対象クラスである
ArticleUseCase
はArticleRepository
に依存しているので、それをMockに置き換える -
coEvery
で、生成したRepositoryのモックインスタンスにパターンを定義する- 引数の型を特に意識しない場合は
any()
を使う
- 引数の型を特に意識しない場合は
-
useCase.getArticles()
はsuspend functionなので、runBlocking
を使う -
coVerify
でメソッドが正しく呼ばれたことのチェックをする -
assertEquals
で狙い通りの結果が得られたことのチェックをする
ViewModelのテスト
先ほどと似ているコードですが、UseCaseのテストと比べると少しだけ厄介です。
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というライブラリを別途追加する必要があります。
testImplementation ‘androidx.arch.core:core-testing:2.1.0’ //追加
これを定義せずにRunすると、下記エラーが出てテストが失敗します。
java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked.
改めてポイントを挙げます。
- テスト対象クラスである
ListViewModel
はArticleUseCase
に依存しているので、それをMockに置き換える -
coEvery
で、生成したuseCaseのモックインスタンスにパターンを定義する -
useCase.load()
はsuspend functionなので、runBlocking
を使う -
coVerify
でメソッドが正しく呼ばれたことのチェックをする -
assertEquals
で狙い通りの結果が得られたことのチェックをするviewModel.kotlinArticles
とviewModel.status
はViewModel内でLiveDataとして定義されているので、それらをテストするためにはInstantTaskExecutorRule
をruleに指定する必要がある
class ListViewModel(private val useCase: ArticleUseCase) : ViewModel(), LifecycleObserver {
val kotlinArticles = MutableLiveData<List<ArticleEntity>>() //LiveDataとして定義
val status = MutableLiveData<Status>() //LiveDataとして定義
...
}
おわりに
今回は簡単なテストをしてみましたが、今後は色々なシナリオを作って、テストを充実させていけたらと思います
手探りでやっておりますので、もし間違っている点や改善できる点などあればコメントいただければ幸いですmm