Android
Kotlin
Mockito

Kotlin、MockitoによるRepositoryパターンのLocal Unit Testについて

こんにちは、

Androidアプリエンジニアの大屋です
最近ようやくテストコードを書き始めたのでテストについてまとめていきたいと思います。

今回はRepositoryパターンのLocal Unit Testについてです。

現在、開発中のアプリですが、MVVM + Repositoryパターンのレイヤードアーキテクチャを採用していてRepositoryパターンの機構を単体テストするにはどうしたらいいのか気になったのでトライして見ました。

よろしくおねがいします💪💪💪

テスト実施用のサンプルアプリ

あと、今回テストするのに簡単なサンプルアプリを用意しました。
アプリといってもSharedPreferencesに書き込み、読み込みをするだけのアプリです。

アプリはGithubリポジトリのrepository-testブランチに公開してますので参考にして下さい。ややこしいですがよろしくお願いします。あと、コードは全てKotlinで実装してます!
full kotlin.png

是非、スターもよろしくお願いします〜⭐️🙏🙏🙏

Repositoryパターンについて...

まずは、Repositoryパターンについて説明していきます。

現在開発中のプロダクトやサンプルアプリではレイヤードアーキテクチャを採用しています。大きく分けるとPresentation層、Domain層、Data層に分かれていて、各層にはそれぞれ役割があります。Presentation層は画面の表示周り、Domain層はビジネスロジック周り、Data層はデータ取得を担当しています。

下図がその概略です。
スクリーンショット 2017-12-14 19.46.31.png

今回はアーキテクチャの記事ではないので設計についての説明は省きますが、図中の赤いのが今回のテスト対象のRepositoryです。

では、Repositoryは一体何をしているのでしょうか?

簡単に言えばDomain層がデータを取得先を意識しなくてもいいように橋渡し役をしています。

つまり、データといってもサーバー側にあったり、ローカルのデータベースにあったり、メモリ内で保持していたりと保存場所だけでも様々なケースが考えられます。そういった、データの取得先についてDomain層が意識しなくてもRepositoryが取得できるところから持ってきてくれるということです。

この、デザインパターンの利点は取得先を選ぶ処理をDomain層のビジネスロジックと一緒に実装しなくてもいい点です。一緒に実装するとコードの可読性が下がります。また、一度取得したデータはRepositoryがキャッシュする為、その後のデータ取得は素早くできる利点もあります。データのキャッシュについてはどのようなデータなのかにもよりますが、Repositoryデザインパターンの利点としては以下の二点が挙げられると思います。

  • ドメイン層がデータの取得先を意識しなくても良い
  • キャッシュ機構を実装するのでデータの取得が速くなる

Repositoryパターンの単体テストで検証すべき機能

下記コードがサンプルアプリのRepositoryとなっています。

サンプルで用意したアプリ自体プリファレンスに書き出し/読み込みをするだけのシンプルなものなので、メソッドも書き込みのfun putString()の読み込みのfun getString()だけです。フィールド変数も一度取得した値を保持しておくキャッシュとプリファレンスにアクセスするキーのみとなっています。

SampleRepository.kt
/**
 * リポジトリクラス
 * Created by toshihirooya on 2017/12/12.
 */
@Singleton
open class SampleRepository @Inject
constructor(private var preference: SamplePreferences) {

    // キャッシュ
    val cache: MutableMap<String, String> = HashMap()
    // プリファレンスキー
    val PREF_KEY = "pref_key"

    fun putString(value: String) {
        preference.putPrefString(PREF_KEY, value)
        cache[PREF_KEY] = value
    }

    fun getString(): Single<String> {
        if (cache.containsKey(PREF_KEY)) {
            return Single.just(cache[PREF_KEY])
        }
        return preference.getPrefString(PREF_KEY)
    }
}

続いて、実際にプリファレンスへの書き込み/読み込みを担当しているクラスは下記になります。

SamplePreferences.kt
/**
 * プリファレンスクラス
 * Created by toshihirooya on 2017/12/12.
 */
@Singleton
open class SamplePreferences @Inject
constructor(context: Context) {
    private val preference = PreferenceManager.getDefaultSharedPreferences(context)

    open fun getPrefString(key: String): Single<String> {
        return Single.just(preference.getString(key, ""))
    }

    open fun putPrefString(key: String, value: String) {
        preference.edit().putString(key, value).apply()
    }
}

サンプルアプリではこれらのクラスが書き込み/読み込みをしているのですが、先ほどまとめたRepositoryパターンの役割を踏まえて大雑把にテストケースを考えると

  • ①Repositoryがデータをキャッシュするテスト
  • ②キャッシュがあればRepositoryはキャッシュを渡すテスト
  • ③キャッシュがなければRepositoryはプリファレンスからデータを取得し渡すテスト

かなと思います。なので、今回この3つのテストケースの検証を持ってRepositoryパターンのLocal Unit Testとしたいと思います。

Mockitoによるmockの差し込み

さて、プロダクトコードからは離れテストコードを書いていこうと思います。

先ほど登場したSampleRepository.ktとSamplePreferences.ktクラスですがSampleRepository.ktのインスタンス生成にSamplePreferences.ktのインスタンスが必要になっています。このRepositoryのインスタンス生成に必要なものをmockで代用してRepositoryのテストを行います。

そこで、Android開発でmockと言えば同じみのmockitoを使っていきたいと思います。

まずは、Repositoryがちゃんとキャッシュするかどうかのテスト

SampleRepositoryTest.kt
    @Test
    fun `リポジトリがキャッシュしてるかのテスト`() {
        val mockSamplePreference = mock(SamplePreferences::class.java)
        val repository = SampleRepository(mockSamplePreference)

        repository.putString("test data")
        Assert.assertEquals("test data", repository.cache[repository.PREF_KEY])
    }

こちらのテストケースでは、生成したRepositoryに対して書き込み処理をし、Repository内のキャッシュを確認するテストとなっています。

続いて、リポジトリがキャッシュを使っているかのテスト

SampleRepositoryTest.kt
    @Test
    fun `リポジトリがキャッシュをつかってるかのテスト`() {
        val mockSamplePreference = mock(SamplePreferences::class.java).apply {
            `when`(getPrefString("pref_key")).thenReturn(Single.just("LocalData"))
        }
        val repository = SampleRepository(mockSamplePreference)

        repository.putString("CashData")

        repository.getString().subscribe({ output ->
            Assert.assertEquals("CashData", output)
        }, {})
    }

こちらのテストケースではRepositoryがキャッシュを使っているかの判断をするためにmockクラスの振る舞いとしてRepositoryからデータを取得する際にLocalDataと言う文字列を返すようにして判断させています。

最後に、リポジトリがキャッシュを保持していない場合にプリファレンスから取得するかのテスト

SampleRepositoryTest.kt
    @Test
    fun `リポジトリがキャッシュを保持していない場合に取得するかのテスト`() {
        val mockSamplePreference = mock(SamplePreferences::class.java).apply {
            `when`(getPrefString("pref_key")).thenReturn(Single.just("LocalData"))
        }
        val repository = SampleRepository(mockSamplePreference)

        repository.getString().subscribe({ output ->
            Assert.assertEquals("LocalData", output)
        }, {})
    }

こちらのテストケースも先ほど同様にmockクラスに対する振る舞いを設定してRepositoryにキャッシュしていない状態でチェックしています。

さて、これらのテストケースをAndroid StudioでLocal Unit Testを実行すると

スクリーンショット 2017-12-15 8.08.22.png

無事、全てのテストケースが完了しました🎉🎉🎉

まとめ

  • Local Unit Testは速く動作してくれて良い
  • Testするにはプロダクトコードの設計が重要