KoinとMockKでAndroidTestを簡単に(Android 9以上)
はじめに
前回Koinについていろいろ調べながら使い方をしらべていたのですがMockitoをAndroidTestで使おうとすると はまりどころが多くて嫌になりました。テストの重要性が上がるにつれ実情に合わなくなってきたのでしょうか。
そんなときAndroidテスト全書をぱらぱらと眺めていたら、Kotlin向けにMockKというライブラリがあるよ、という文字が目に入りました。
さらにうれしいのがAndroidのサポート状況です。
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でモックオブジェクトを作ることができます。