1. Home
  2. テクノロジー
  3. Mokcito-KotlinでAndroidアプリのテストコードを書いてみた

Mokcito-KotlinでAndroidアプリのテストコードを書いてみた

目次

この記事は、ニフティグループ Advent Calendar 2023, 19日目の記事です。

はじめに

こんにちは。「ニフティ不動産」アプリのAndroid側の開発を担当している、TS-Elfenです。今回は、 Mockito-Kotlinを使った、Androidアプリのユニットテストについて紹介したいと思います。Mockito-Kotlinを使うと、外部のライブラリに依存している実装にもテストコードを書くことができるようになります。

Mockito/Mockito-Kotlinとは?

MockitoはJavaのユニットテストのために開発された、モックフレームワークです。モックフレームワークとは、システムの一部をテストするために、システムに依存している部分をモックとして置き換えるためのフレームワークです。Mockito はMITLicenseのライブラリであり、2023/11/27時点で14.3kのStarを有しています。

MockitoはJava用のため、Kotlinでそのまま使用するには使いにくい部分があります。例えば、whenが予約語となっているKotlinのコードでは、下記のようにエスケープシーケンスをつける必要があったり、Nullableの取り扱いがJavaとKotlinで異なるため、特有のエラーが起きるなどの問題も起きます。

val sharedPreferences = mock(SharedPreferences::class.java)
val editor = mock(SharedPreferences.Editor::class.java, RETURNS_DEEP_STUBS)

`when`(sharedPreferences.edit()).thenReturn(editor)
`when`(editor.commit()).thenReturn(true)
`when`(editor.putString(anyString(), anyString())).thenReturn(editor)

Mockito-Kotlinは、MockitoをKotlin向けに使いやすくしたライブラリです。Niek Haarmanさんが作ったライブラリで、2023/11/27時点で3kのStarを有しています。

使用方法&ユースケースごとのサンプル

Mockito-Kotlinを用いたユニットテストの書き方について、事前準備と代表的な使い方を紹介します。

事前準備

まず、 app配下のbuild.gradleに下記を記載します。この時、JVMのバージョンなどでうまくインポートができない場合があるので、自身のプロジェクトの状況に応じてバージョンを調整してください。

testImplementation "org.mockito.kotlin:mockito-kotlin:4.1.0"

次に、UT作成の障壁となっている外部依存のオブジェクトを外から渡すように修正します。

// Before 
import org.hoge.ExterenalInstance

class HogeDatastore() {
  val exterenalInstance = ExterenalInstance.newInstance()
  
  fun getPower() = externalInstance.getPower() // 外部パッケージのインスタンスを内部生成して、メソッド呼び出し
  
}

例えば、上記のような書き方をしていると、getPowerのテストを書きたい!と思っても、ユニットテストのコンテキストから ExternalInstanceを呼び出しても思った返却値が得られない、呼び出してもExceptionを出力してしまうといった可能性があります。そこで、下記のように、外部のクラスを用いる場合は、引数として受け取るように書き換えるようにします。

import org.hoge.ExterenalInstance

class HogeDatastore @Inject constructor(
  private val externalInstance : ExternalInstance//★外部から渡せるようにしておく、Hilt導入済みならModuleを作る
) {
  fun getPower() = externalInstance.getPower() // 外部パッケージのインスタンスを内部生成して、メソッド呼び出し
}
import org.hoge.ExterenalInstance
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
class ExternalInstanceModule {

    @Provides
    fun provideExternalInstance() : ExternalInstance = ExtrenalINstance.newInstance()
}

これによって、モックオブジェクトを用いたユニットテストを書くことができます。

オブジェクトの特定メソッドの戻り値を適当なものに変えたい場合

val mockSharedPreference = mock<SharedPreferecens> { // ★Mock化するインスタンスの指定。
            on { 
              getString(Mockito.any())  // ★Mock化したインスタンスの、挙動を変更するメソッドの指定。
            } doReturn "Test"           // ★戻り値の指定
            on { 
              getBoolean(Mockito.anyString())  // ★複数ある場合はon{}を繋げて書けばOK
            } doReturn false           
        }
        val dataStore = AppConfigurationDataStore( RuntimeEnvironment.systemContext, mockSharedPreference)
        dataStore.loadStringValue("testKey").collect { result ->
            result.onSuccess {
                Truth.assertThat(it).isEqualTo("Test")
            }
            result.onFailure {
                assert(false)
            }
        }

mock<XXX>で任意のオブジェクトをモックにし、インラインで挙動を定義することができます。 on{ }ブロックの中に変更したいメソッド名を記載し、引数を Mockito.anyXXX()で指定します。今回は、String型を受け取る想定なので anyString()を使います。この時、型を合わせないとエラーが発生する場合があるので注意しましょう。ブロックの後に、 doReturnをつけて返したい値を書けば完了となります。

例外をテストしたい場合

@Test
    fun MockitoTest2() = runBlocking {
        val mockSharedPreference = mock<SharedPreferecens> {
            on { getString(Mockito.any()) } doThrow Exception("")
        }
        val dataStore = AppConfigurationDataStore( RuntimeEnvironment.systemContext, mockSharedPreference)
        dataStore.loadStringValue("testKey").collect { result ->
            result.onSuccess {
                assert(false)
            }
            result.onFailure {
                assert(true)
            }
        }
    }

外部インスタンスが出力するエラーに対して、自身のコードがちゃんと対処できているかを確認することを考えます。この場合は、doThrow で例外を出力するように書き換えることが可能です。書き換えた後に、DataStoreなどで、エラー処理がちゃんと行えてるかを確認するだけでOKです。

他メソッドはそのまま一部メソッドの挙動を変更してテストしたい場合

private interface MyInterface {
        fun getString(value: String): String
    }
    private open class Foo: MyInterface {
        fun yeah(value: String): String = "yeah"
        override fun getString(value: String): String = value
    }
    @Test
    fun MockitoTest() = runBlocking {
        val foo = spy<Foo> {
            on { getString(Mockito.anyString()) } doReturn "hoge"
        }
        Truth.assertThat(foo.getString("")).isEqualTo("hoge")
        Truth.assertThat(foo.yeah("")).isEqualTo("yeah") //★いじっていないyeahメソッドは通常通り動作する
    }

mockではなくspyを使うと、変更を要求したメソッドや変数以外は通常通りの動作になります。 mockでは、変更していないメソッドは全てnopになってしまうので注意してください。

メソッドの呼び出し回数をテストしたい場合

    @Test
    fun MockitoTest3() = runBlocking {
        val mockSharedPreference = mock<SharedPreferecens> {
            on { getString(Mockito.any()) } doReturn ""
        }
        val dataStore = AppConfigurationDataStore( RuntimeEnvironment.systemContext, mockSharedPreference)
        dataStore.loadStringValue("testKey").collect {
            verify(mockSharedPreference, times(1)).getString("testKey")
        }
    }

例えば、DataStore内で、特定の回数、特定のメソッドが呼ばれることを確認したい場合は、verifytimesを使います。verify(<調べたいMock>、times(<呼び出し回数>)).<調べたいメソッド>でそのMockの特定メソッドがどれだけ呼ばれているかを調べることができます。

外部メソッドに渡されている引数をテストしたい場合

private interface MyInterface {
        fun getString(value: String): String
    }
    
    private open class StringAdder: MyInterface {
        fun add (value: String) : String = getString("add" + value)
        override fun getString(value: String): String = "getString" + value
    }
    
    @Test
    fun MockitoTest() = runBlocking {
        val stringAdder = spy<StringAdder> {
            on { getString(Mockito.anyString()) } doReturn "hoge"
        }
        stringadder.add("fuga")
        argumentCaptor<String>().apply {
            verify(stringadder).getString(capture())
            Truth.assertThat(allValues).containsExactly("addfuga")
        }
        Truth.assertThat(stringadder.getString("")).isEqualTo("hoge")
    }

文字列を取得するメソッド getStringを持っている MyInterfaceというインターフェースがいたとします。それを継承した、 StringAdderには、 getStringを文字列を付与してから呼び出す addというメソッドがいます。この時、 getStringが、自分が作成した addの想定した呼び方がされているかを調べることを想定します。

この時は、argumentCaptorという枠組みを使います。まず、調べたいメソッドを呼び出します。その後、argumentCaptor<引数の型>().apply{ }ブロックの中で、またverifyメソッドを呼び出します。この時、調べたいメソッドの引数に capture()を入力します。そして、その後は allValuesでキャプチャされた引数を全取得することができるので、想定通りの引数が入っているかをassertで検証することで、引数に対するテストを行うことができます。

まとめ

本記事では、Mockito-Kotlinの代表的な使い方について紹介しました。SharedPreferenceFirebaseRemoteConfigなどからの返却値を使ったコードでは、その返却値を擬似的なものに変えることが難しく、テストコードが書けなかったのですが、Mockito-Kotlinでその課題は解消できました。今後、プロダクトの品質向上に役立てたいと思っています。Mockito-Kotlinには、今回紹介した以外にもさまざまな使い道があるようなので、ぜひ皆さんも調べて使ってみてください!

参考にさせていただいたURL

この記事をシェア

掲載内容は、記事執筆時点の情報をもとにしています。