androidのテストを書きたい
テストのコードラボやドキュメントを読みながらテストの書き方を勉強して自分で作成したアプリにテストコードを導入してみた。
ただコードラボは書き方が変わっている部分があったり、使っているライブラリなどによって躓いたこともあったので、調べたことや参考にした記事などまとめていく。
Advanced Android in Kotlin 05.1: Testing Basics | Android Developers
Advanced Android in Kotlin 05.2: Introduction to Test Doubles and Dependency Injection | Android Developers
Advanced Android in Kotlin 05.3: Testing Coroutines and Jetpack integrations | Android Developers
LiveData
InstantTaskExecutorRule()を追加する
アーキテクチャコンポーネント(AC)関連のテストの時に追加する。
ACがbackground executorをテスト用に変換して、テスト結果が同期的に順番に得られるようにする。
testImplementation "android.arch.core:core-testing:1.1.0"
@get:Rule val rule: TestRule = InstantTaskExecutorRule()
ObserverForever()で値を監視する
これを使うことでlifecyclerOwnerなしでliveDataの監視ができる。
val observer = Observer<Int>{} try { sampleViewModel.data.observeForever(observer) ... }finally { sampleViewModel.data.removeObserver(observer) }
参考
Advanced Android in Kotlin 05.1: Testing Basics | Android Developers
InstantTaskExecutorRule | Android Developers
LiveDataのUnitTest. ちょっとやっかいな部分もあったので、LiveDataのテストをどう書くのかまとめ… | by Kenji Abe | Medium
以下の記事はliveDataのobserverの設定と解除、値の取得をUtilでまとめる方法
Unit-testing LiveData and other common observability problems | by Jose Alcérreca | Android Developers | Medium
Coroutine
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
runTest
コルーチンを含むテスト全体をrunTestでラップする。
suspend fun fetchData(): String { delay(1000L) return "Hello world" } @Test fun dataShouldBeHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) }
メインディスパッチャの変更
UnitTestではローカル JVM 上で実行されるため、Android UI スレッドをラップする Mainディスパッチャは使用できず、テスト対象のコードがメインスレッドを参照すると、単体テスト中に例外がスローされる。
そのためMainディスパッチャをTestDispatchersに置き換える必要がある。
この操作は各UnitTestで重複するため、テストルールとして抽出しておく。
class MainDispatcherRule( val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), ) : TestWatcher() { override fun starting(description: Description) { Dispatchers.setMain(testDispatcher) } override fun finished(description: Description) { Dispatchers.resetMain() } } class HomeViewModelTestUsingRule { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun settingMainDispatcher() = runTest { ... } }
TestDispatchers
新しく開始されるコルーチンのスケジューリング方法が異なる2つのTestDispatchersがある。
- StandardTestDispatcher
テストスレッドが空いているときにコルーチンを実行する。
テスト中の新規コルーチンの動作を細かく制御でき、本番環境のコードにおけるコルーチンのスケジューリングに似ているため、同時実行を重視してみる時に使用。
- UnconfinedTestDispatcher
新しいコルーチンが開始されると、現在のスレッドで積極的に開始する。
コルーチンを使用した単純なテストをするだけならこっちで良い。
参考
Testing Kotlin coroutines on Android | Android Developers
Hilt
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.44' kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.44'
@HiltAndroidTestとHiltAndroidRuleを追加する
Hilt を使用する UI テストには @HiltAndroidTest アノテーションをつけHilt コンポーネントを生成させる。
テストクラスに HiltAndroidRuleを追加するが、他にも追加するテストルールがある時はルールを1つにまとめるか、HiltAndroidRuleを最初に実行する。
@get:Rule var rule = RuleChain.outerRule(HiltAndroidRule(this)). around(SettingsActivityTestRule(...))
@get:Rule(order = 0) var HiltAndroidRule = HiltAndroidRule(this) @get:Rule(order = 1) var settingsActivityTestRule = SettingsActivityTestRule()
Applicationを設定する
インストゥルメンテーションテストの場合は新しいテストランナーを作成してGradleファイルでテストランナーを設定する。 (Robolectricを使ったテストの時はまた違った書き方だけどやってみてないので省略)
class CustomTestRunner : AndroidJUnitRunner() { override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { return super.newApplication(cl, HiltTestApplication::class.java.name, context) } }
android { defaultConfig { testInstrumentationRunner "com.example.android.dagger.CustomTestRunner" //作成したテストランナーのclass path } }
テストに型を注入する
@Injectをつけた型を注入するよう、hiltRule.inject()を実行する。
@Inject lateinit var analyticsAdapter: AnalyticsAdapter @Before fun init() { hiltRule.inject() }
本番環境とは違うモックインスタンスを注入したい場合は、擬似依存関係を定義した新しいHiltモジュールを作成して、上記と同様に注入する。
@Module @TestInstallIn( components = [SingletonComponent::class], replaces = [AnalyticsModule::class] //置き換えたい本番用のHiltモジュールを指定する ) abstract class FakeAnalyticsModule { @Singleton @Binds abstract fun bindAnalyticsService(fakeAnalyticsService: FakeAnalyticsService): AnalyticsService // FakeAnalyticsServiceを代わりに使うようにバインディング }
これでHiltを使ったテスト全てでモックのインスタンスに置き換わる。 単一のテストだけ置き換えたい時は、テストに@UninstallModulesアノテーションをつけてモジュールを使わないように設定し、そのテスト内で新しくモジュールを定義する。
@UninstallModules(AnalyticsModule::class) @HiltAndroidTest class SettingsActivityTest { @Module @InstallIn(SingletonComponent::class) abstract class TestModule { @Singleton @Binds abstract fun bindAnalyticsService( fakeAnalyticsService: FakeAnalyticsService ): AnalyticsService }
※余談
実行時「error: cannot find symbol import dagger.hilt.android.internal.testing.root.DaggerDefault_HiltComponents_SingletonC;」が出た。
(これと同じエラーHilt, androidTest build failed in versions >= 2.40.4 · Issue #3162 · google/dagger · GitHub)
依存関係まわりを色々変更してみようと試行錯誤していたけど、自分の場合はFakeで作ったクラス(上のコードでいうFakeAnalyticsService)に@Inject constructor() をつけ忘れてただけだった。