質問
viewModelScope
Kotlinコルーチンを使用してAndroid単体テストに注入するための最良の戦略は何ですか?単体テストのために CoroutineScope が ViewModel に挿入されるとき、CoroutineDispatcher も挿入され
flowOn
、実稼働コードで必要とされない場合でも使用して定義されるべきですか?
flowOn
Dispatchers.IO
RetrofitはSomeRepository.ktでスレッド化を処理し、viewModelScope
は でデータを返すため、このユース ケースのプロダクション コードでは必要ありませんDispathers.Main
。どちらもデフォルトで行われます。
期待される
Kotlin Flow 値に保存された Android の ViewModel ビューステート値で単体テストを実行します。
観察した
メイン ディスパッチャを含むモジュールの初期化に失敗しました。テストには、kotlinx-coroutines-test モジュールの Dispatchers.setMain を使用できます
CoroutineScope がハードコードされている最初の発生時に、単体テストが失敗します。viewModelScope
起動されたコルーチンが ViewModel のライフサイクルを維持するように利用されます。ただし、viewModelScope
ViewModel 内から作成されるため、ViewModel の外部で定義して引数として渡すことができる CoroutineDispatcher と比較して、注入がより複雑になります。
実装
SomeViewModel.kt
fun bindIntents(view: FeedView) {
view.initStateIntent().onEach {
initState(view)
}.launchIn(viewModelScope)
}
SomeTest.kt
@ExperimentalCoroutinesApi
class SomeTest : BeforeAllCallback, AfterAllCallback {
private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
private val repository = mockkClass(FeedRepository::class)
private var loadNetworkIntent = MutableStateFlow<LoadNetworkIntent?>(null)
override fun beforeAll(context: ExtensionContext?) {
// Set Coroutine Dispatcher.
Dispatchers.setMain(testDispatcher)
}
override fun afterAll(context: ExtensionContext?) {
Dispatchers.resetMain()
// Reset Coroutine Dispatcher and Scope.
testDispatcher.cleanupTestCoroutines()
testScope.cleanupTestCoroutines()
}
@Test
fun topCafesPoc() = testDispatcher.runBlockingTest {
coEvery {
repository.getInitialCafes(any())
} returns mockGetInitialCafes(mockCafesList, SUCCESS)
val viewModel = FeedViewModel(repository)
viewModel.bindIntents(object : FeedView {
@ExperimentalCoroutinesApi
override fun initStateIntent() = MutableStateFlow(true)
@ExperimentalCoroutinesApi
override fun loadNetworkIntent() = loadNetworkIntent.filterNotNull()
override fun render(viewState: FeedViewState) {
// TODO: Test viewState
}
})
loadNetworkIntent.value = LoadNetworkIntent(true)
// TODO
// assertEquals(4, 2 + 2)
}
}
注: JUnit 5 テスト拡張機能は、最終バージョンで使用されます。
完全なエラー ログ
スレッド「main @coroutine#1」での例外 java.lang.IllegalStateException: メイン ディスパッチャを含むモジュールの初期化に失敗しました。テストでは、kotlinx-coroutines-test モジュールの Dispatchers.setMain を kotlinx.coroutines.internal.MissingMainCoroutineDispatcher.missing(MainDispatchers.kt:113) で使用できます。 kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:285) kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109) at kotlinx.coroutines .AbstractCoroutine.start(AbstractCoroutine.kt:158) at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders. 68) com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33) で com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230) で com.intellij.rt.junit で.JUnitStarter.main(JUnitStarter.java:58) 原因: java.lang.RuntimeException: android.os.Looper のメソッド getMainLooper がモックされていません。見るhttp://g.co/androidstudio/not-mocked詳細については。android.os.Looper.getMainLooper(Looper.java) で kotlinx.coroutines.android.AndroidDispatcherFactory.createDispatcher(HandlerDispatcher.kt:55) で kotlinx.coroutines.android.AndroidDispatcherFactory.createDispatcher(HandlerDispatcher.kt:52) で kotlinx. coroutines.internal.MainDispatchersKt.tryCreateDispatcher(MainDispatchers.kt:57) at kotlinx.coroutines.test.internal.TestMainDispatcher.getDelegate(MainTestDispatcher.kt:19) at kotlinx.coroutines.test.internal.TestMainDispatcher.getImmediate(MainTestDispatcher.kt: 32) androidx.lifecycle.ViewModelKt.getViewModelScope(ViewModel.kt:42) で ... スレッド「main @coroutine#1」で 40 以上の例外 java.lang.IllegalStateException: Main ディスパッチャを持つモジュールの初期化に失敗しました。テストディスパッチャ用。com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58) の prepareStreamsAndStart(JUnitStarter.java:230) 原因: java.lang.RuntimeException: android.os.Looper のメソッド getMainLooper がモックされていません。見る詳細については、http: //g.co/androidstudio/not-mockedをご覧ください。android.os.Looper.getMainLooper(Looper.java) で kotlinx.coroutines.android.AndroidDispatcherFactory.createDispatcher(HandlerDispatcher.kt:55) で kotlinx.coroutines.android.AndroidDispatcherFactory.createDispatcher(HandlerDispatcher.kt:52) で kotlinx. coroutines.internal.MainDispatchersKt.tryCreateDispatcher(MainDispatchers.kt:57) at kotlinx.coroutines.test.internal.TestMainDispatcher.getDelegate(MainTestDispatcher.kt:19) at kotlinx.coroutines.test.internal.TestMainDispatcher.getImmediate(MainTestDispatcher.kt: 32) app.topcafes.feed.viewmodel.FeedViewModel.bindIntents(FeedViewModel.kt:38) で androidx.lifecycle.ViewModelKt.getViewModelScope(ViewModel.kt:42) ... 39 もっと見る