AndroidプロジェクトでのUnitTest環境をセットアップする

(最近の)Androidのプロジェクトにおいて、新しくUnitTestを書くところまでのセットアップのトラブルシューティングを残しておく。

環境

Android Studio: v4.0 使用しているTest Libraryとversionは以下の通り

  • "org.mockito:mockito-core:2.23.0"
  • "com.nhaarman.mockitokotlin2:mockito-kotlin:2.0.0"
  • 'androidx.test.ext:junit:1.1.1'
  • 'androidx.test.espresso:espresso-core:3.2.0'
  • "org.mockito:mockito-android:2.23.0"
  • "android.arch.core:core-testing:2.1.0-alpha02"
  • "com.jraska.livedata:testing-ktx:1.1.2"
  • "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.4"
  • "org.robolectric:robolectric:4.3"

Cannot invoke observeForever on a backgground thread

以下のコードのようにViewModelのUnitTestでLiveDataのobserveされる値をテストする時に発生した。

 @Test
    fun `getCountryList_success`() {
        val list = listOf("USA")
        repository = mock {
            onBlocking { fetchCountryList() }.thenReturn(list)
        }

        viewModel = MainViewModel(repository, Dispatchers.Main)
        val testObserver = viewModel.countryList.test()

        runBlocking {
            viewModel.getCountryList()
            val history = testObserver.valueHistory()

            Assert.assertEquals(history[0], list)
            verify(repository).fetchCountryList()
        }
    }

解決策

DroidKaigi/conference-app-2020ViewModelTestRule を参考にTestRuleを定義/設定する。

    @Rule
    @JvmField
    val viewModelTestRule = ViewModelTestRule()

MockitoException Mockito cannot mock/spy because : final class

ViewModelのテストでRepositoryをMockしようとした時に発生。 MockしようとしていたRepositoryはKotlinで書かれたclassであったため、final class扱いとなっていたため。

解決策

  1. Repositoryにopenをつけて継承可能にする。 Mockは無事できるようになるが、UnitTestのためにプロダクトコードのfinal classの状態を変更するのはあまり良い解決策とは思えない。
open class CountryRepository{

}
  1. Interfaceに切ってTestではinterfaceをmockする
interface CountryRepository{
}

class CountryRepositoryImpl() : CountryRepository{
}

ViewModelの引数ではInterfaceの型で受けるようにし、UnitTestではinterfaceの型をmockする。

@Mock
lateinit var repository: CountryRepository

Koinで設定したDIには as でどのinterfaceとして扱われるかを明示しておくとプロダクションコードでも問題なく動く。

val repositoryModule = module {
    single { CountryRepositoryImpl(get(), get(), get()) as CountryRepository }
}
  1. その他

mockito-extensions というresource directoryを切って設定する方法もあるみたいだが、自分は解決されなかった。おとなしくinterfaceに切るのがTestableな設計になるのだと思う。

KotlinクラスをMockito2でMockするには - Qiita

IllegalArgumentException: API level 29 is not available

Robolectric起因のErrorで何回も遭遇する…

java.lang.IllegalArgumentException: API level 29 is not available · Issue #5207 · robolectric/robolectric · GitHub

解決策

src/test/recources の配下に robolectric.properties というfileを作成し以下を記入

sdk=28

@Config をTest classごとにつけてもOKだがすべてのTest classに個別に追加しなくてはいけないので共通の設定は properties fileに記しておく。

A KoinContext is already started

Koinに限らずApplicationクラスで定義されているinit処理系でerrorになったら同じ問題。 AndroidTestは特にTestが走るたびにApplicationも起動してしまうのでTest用のApplicationに差し替える。

解決策

Robolectric 3.0でApplicationクラスやConstantsクラスをrobolectric.propertiesに書き出す時の注意点 - Tatuas Blog

test directoryにTest用のTestApplication.kt を作成する

robolectric.properies

application=com.chiiia12.sampleapp.TestApplication

の定義を追加すると解決する。

などなど、普段1からセットアップする機会もなかったりするのでテストコードを書くまでに時間がかかると萎えるので今後同じことで詰まった時に参照したし。