虚苦心観察ブログ

ブログ管理者である虚苦心が私利私欲に基づいて書いているブログです。主にガジェットのレビューだったり、画像処理のことだったりを記事にしています。

KoinとMockKでAndroidTestを簡単に(Android 9以上)

はじめに

前回Koinについていろいろ調べながら使い方をしらべていたのですがMockitoをAndroidTestで使おうとすると はまりどころが多くて嫌になりました。テストの重要性が上がるにつれ実情に合わなくなってきたのでしょうか。

そんなときAndroidテスト全書をぱらぱらと眺めていたら、Kotlin向けにMockKというライブラリがあるよ、という文字が目に入りました。

mockk.io

さらにうれしいのがAndroidのサポート状況です。

mockk.io

Android9上でのAndroidTestに関してはPowerMockのようなライブラリを必要とすることなくPrivateメソッドなどのテストを行えるのです。
これはとてもうれしい。

テスト方法

Mockitoに比べてテストを書く上での決まり事が少なくい印象です。 例えばRunWithにMockitoJUnitTestRunnerを指定しなくてよくなっています。 またAnnotationを使ったりするためにMockitoAnnotations.initMocks(this)が必要でしたが不要になっています。

KoinとMockKを使ってテストを書いた場合は以下になります。

@RunWith(AndroidJUnit4ClassRunner::class)
class MyFragmentTest : KoinTest {

    fun MyFragment起動時にPresenterのonViewCreatedとonResumeが呼ばれる() {
        loadKoinModules(listOf(module(override = true) {
            factory<MytContract.Presenter>(override = true) { (view: MyContract.View) ->
                spyk(MyPresenter(view, get()))
            }

            single<MyContract.Repository>(override = true) {
                spyk(MyRepository())
            }
        }))

        val scenario = launchFragment<MyFragment>()

        scenario.onFragment {
            val mockPresenter = it.presenter
            verifySequence {
                mockPresenter.onCreateView()
                mockPresenter.onViewCreated()
                mockPresenter.view
                mockPresenter.onResume()
            }
        }
    }

    @Test
    fun MyFragment起動時にPresenterがrequestを呼ぶ() {
        loadKoinModules(listOf(module(override = true) {
            factory<MyContract.Presenter>(override = true) { (view: MyContract.View) ->
                spyk(MyPresenter(view, get()), recordPrivateCalls = true)
            }

            single<MyContract.Repository>(override = true) {
                spyk(MyRepository(), recordPrivateCalls = true)
            }
        }))

        val scenario = launchFragment<MyFragment>()

        scenario.onFragment {
            val mockPresenter = it.presenter
            verify(exactly = 1) {
                mockPresenter["requestFirstPageIfNotRequested"]()
                mockPresenter["requestFirstPage"]()
                mockPresenter["request"](1)
            }
        }
    }
}

特徴的なところをいくつか紹介します。

spyk

今回はmockk(mockitoにおけるmock)は使わずspykのみを使っています。
今回の例ではテストを2つ書いています。 1つ目はpublicメソッドのみ 2つ目はprivateメソッドのみです。

publicメソッドのみ

publicメソッドのみを使ったテストは難しいことはありません。
今回の場合はFragmentがResumeされるまでの動作を検証します。
FragmentがResumeされるまでにPresenterは 1. onCreateView 2. onViewCreated 3. view(プロパティの呼び出し) 4. onResume を呼び出します。 この順番で呼ばれることを検証したい場合、MockKではverifySequenceを使用します。 verifySequenceを使ったテストの抜粋です。

verifySequence {
    mockPresenter.onCreateView()
    mockPresenter.onViewCreated()
    mockPresenter.view
    mockPresenter.onResume()
}

verifySequenceの特徴は呼ばれるメソッドが抜けもれなく書かれている必要があることと順番にも依存することです。 たとえばmockPresenter.onViewCreated()が書かれていない以下のようなテストでは失敗します。

verifySequence {
    mockPresenter.onCreateView()
    mockPresenter.view
    mockPresenter.onResume()
}

同様にメソッドに抜けもれがなかったとしても順番が間違っていても失敗します。 たとえばonCreateViewとonViewCreatedの順番が入れ替わっている以下のようなテストでは失敗します。

verifySequence {
    mockPresenter.onViewCreated()
    mockPresenter.onCreateView()
    mockPresenter.view
    mockPresenter.onResume()
}

privateメソッドも含んだテスト

privateメソッドをテストする場合はspykメソッドの引数にrecordPrivateCalls = trueを指定します。
こうすることでprivateメソッドもテストすることが可能になります。
テスト時のprivateメソッドの検証は以下のようになります。

mockPresenter["requestFirstPageIfNotRequested"]()

publicメソッドはメソッドとして呼び出していましたが、privateメソッドでは文字列を指定することでモックオブジェクトから間接的に呼び出すことになります。
実際のテストコードでは以下のようになります。

verify(exactly = 1) {
   mockPresenter["requestFirstPageIfNotRequested"]()
   mockPresenter["requestFirstPage"]()
   mockPresenter["request"](1)
}

今回はverifySequenceを使わずverifyを使っています。
verifySequenceとの違いはメソッド呼び出しの順番や呼び出されるメソッドの抜けもれを意識しなくていい点です。
exectly = 1はメソッドが呼び出される回数を指定しています。
このように書くことでprivateメソッドをテストすることが可能です。

ではverifyではなくpublicメソッドと同じくverifySequenceを使った場合どうなるでしょうか?

verifySequence {
    mockPresenter.onCreateView()
    mockPresenter.onViewCreated()
    mockPresenter.view
    mockPresenter.onResume()
}

これは失敗します。なぜならprivateメソッドもモックオブジェクトが監視しているためです。
privateを含めたverifySequenceのテストは以下のようになります。

verifySequence {
    mockPresenter.onCreateView()
    mockPresenter.onViewCreated()
    mockPresenter.view
    mockPresenter.onResume()
    mockPresenter["requestFirstPageIfNotRequested"]()
    mockPresenter["requestFirstPage"]()
    mockPresenter["request"](1)
}

Koinのテスト時の書き方

KoinはDIライブラリです。
今回はFragment内で使っているPresenterとRepositoryをモックオブジェクトにするために使っています。
Koinを使ったDIの抜粋です。

loadKoinModules(listOf(module(override = true) {
   factory<MytContract.Presenter>(override = true) { (view: MyContract.View) ->
      spyk(MyPresenter(view, get()))
   }

   single<MyContract.Repository>(override = true) {
      spyk(MyRepository())
   }
}))

loadKoinModulesでDIするオブジェクトを指定しています。
KoinではDIするオブジェクトの初期化はstartKoinメソッドで行います。しかしstartKoinは1回しか呼び出すことができません。
startKoinはApplicationクラスで呼び出しているためテスト時に呼び出すことができないのです。

そのためテスト時にはstartKoinメソッドの代わりにloadKoinメソッドを呼び出します。
使い方はstartKoinメソッドと同じですが、startKoinで指定済みのオブジェクトを上書きする必要があるので、module、factory、singleでは引数にoverride = trueを指定しています。
これによりテスト時に対象のオブジェクトを置き換えることが可能です。
今回は置き換えにspykメソッドを使っています。
またKotlinではクラスは基本的にfinalになります。mockitoではopenでクラスを作成しないとspyすることはできませんでしたがMockKではopenせずにspykでモックオブジェクトを作ることができます。