使用Kotlin协程为Android单元测试注入viewModelScope
的最佳策略是什么?
将CoroutineScope注入ViewModel进行单元测试时,即使在生产代码中不需要,也应该使用flowOn
注入和定义CoroutineDispatcher吗?
flowOn
,因为Retrofit在 SomeRepository.kt 中处理Dispatchers.IO
上的线程,并且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.internal.MissingMainCoroutineDispatcher.isDispatchNeeded(MainDispatchers.kt:91) 在kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:285) 在kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) 在kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109) 在kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:158) 在kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:56) 在kotlinx.coroutines.BuildersKt.launch(来源不明) 在kotlinx.coroutines.BuildersKt__Builders_commonKt.launch $ default(Builders.common.kt:49) 在kotlinx.coroutines.BuildersKt.launch $ default(未知来源) 在kotlinx.coroutines.flow.FlowKt__CollectKt.launchIn(Collect.kt:49) 在kotlinx.coroutines.flow.FlowKt.launchIn(未知来源) 在app.topcafes.feed.viewmodel.FeedViewModel.bindIntents(FeedViewModel.kt:38) 在app.topcafes.FeedTest $ topCafesPoc $ 1.invokeSuspend(FeedTest.kt:53) 在app.topcafes.FeedTest $ topCafesPoc $ 1.invoke(FeedTest.kt) 在kotlinx.coroutines.test.TestBuildersKt $ runBlockingTest $ deferred $ 1.invokeSuspend(TestBuilders.kt:50) 在kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) 在kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56) 在kotlinx.coroutines.test.TestCoroutineDispatcher.dispatch(TestCoroutineDispatcher.kt:50) 在kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:288) 在kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) 在kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109) 在kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:158) 在kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:91) 在kotlinx.coroutines.BuildersKt.async(未知来源) 在kotlinx.coroutines.BuildersKt__Builders_commonKt.async $ default(Builders.common.kt:84) 在kotlinx.coroutines.BuildersKt.async $ default(未知来源) 在kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:49) 在kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:80) 在app.topcafes.FeedTest.topCafesPoc(FeedTest.kt:47) 在sun.reflect.NativeMethodAccessorImpl.invoke0(本机方法)处 在sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 在sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 在java.lang.reflect.Method.invoke(Method.java:498) 在org.junit.runners.model.FrameworkMethod $ 1.runReflectiveCall(FrameworkMethod.java:50) 在org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) 在org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) 在org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) 在org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) 在org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) 在org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57) 在org.junit.runners.ParentRunner $ 3.run(ParentRunner.java:290) 在org.junit.runners.ParentRunner上$ 1.schedule(ParentRunner.java:71) 在org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) 在org.junit.runners.ParentRunner.access $ 000(ParentRunner.java:58) 在org.junit.runners.ParentRunner上$ 2.evaluate(ParentRunner.java:268) 在org.junit.runners.ParentRunner.run(ParentRunner.java:363) 在org.junit.runner.JUnitCore.run(JUnitCore.java:137) 在com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java: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) 在kotlinx.coroutines.test.internal.TestMainDispatcher.getDelegate(MainTestDispatcher.kt:19) 在kotlinx.coroutines.test.internal.TestMainDispatcher.getImmediate(MainTestDispatcher.kt:32) 在androidx.lifecycle.ViewModelKt.getViewModelScope(ViewModel.kt:42) ...另外40个 线程“主@ coroutine#1”中的异常java.lang.IllegalStateException:具有主调度程序的模块无法初始化。对于测试,可以使用kotlinx-coroutines-test模块中的Dispatchers.setMain 在kotlinx.coroutines.internal.MissingMainCoroutineDispatcher.missing(MainDispatchers.kt:113) 在kotlinx.coroutines.internal.MissingMainCoroutineDispatcher.isDispatchNeeded(MainDispatchers.kt:91) 在kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:285) 在kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) 在kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109) 在kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:158) 在kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:56) 在kotlinx.coroutines.BuildersKt.launch(来源不明) 在kotlinx.coroutines.BuildersKt__Builders_commonKt.launch $ default(Builders.common.kt:49) 在kotlinx.coroutines.BuildersKt.launch $ default(未知来源) 在kotlinx.coroutines.flow.FlowKt__CollectKt.launchIn(Collect.kt:49) 在kotlinx.coroutines.flow.FlowKt.launchIn(未知来源) 在app.topcafes.feed.viewmodel.FeedViewModel.bindIntents(FeedViewModel.kt:42) 在app.topcafes.FeedTest $ topCafesPoc $ 1.invokeSuspend(FeedTest.kt:53) 在app.topcafes.FeedTest $ topCafesPoc $ 1.invoke(FeedTest.kt) 在kotlinx.coroutines.test.TestBuildersKt $ runBlockingTest $ deferred $ 1.invokeSuspend(TestBuilders.kt:50) 在kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) 在kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56) 在kotlinx.coroutines.test.TestCoroutineDispatcher.dispatch(TestCoroutineDispatcher.kt:50) 在kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:288) 在kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) 在kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109) 在kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:158) 在kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:91) 在kotlinx.coroutines.BuildersKt.async(未知来源) 在kotlinx.coroutines.BuildersKt__Builders_commonKt.async $ default(Builders.common.kt:84) 在kotlinx.coroutines.BuildersKt.async $ default(未知来源) 在kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:49) 在kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:80) 在app.topcafes.FeedTest.topCafesPoc(FeedTest.kt:47) 在sun.reflect.NativeMethodAccessorImpl.invoke0(本机方法)处 在sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 在sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 在java.lang.reflect.Method.invoke(Method.java:498) 在org.junit.runners.model.FrameworkMethod $ 1.runReflectiveCall(FrameworkMethod.java:50) 在org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) 在org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) 在org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) 在org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) 在org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) 在org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57) 在org.junit.runners.ParentRunner $ 3.run(ParentRunner.java:290) 在org.junit.runners.ParentRunner上$ 1.schedule(ParentRunner.java:71) 在org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) 在org.junit.runners.ParentRunner.access $ 000(ParentRunner.java:58) 在org.junit.runners.ParentRunner上$ 2.evaluate(ParentRunner.java:268) 在org.junit.runners.ParentRunner.run(ParentRunner.java:363) 在org.junit.runner.JUnitCore.run(JUnitCore.java:137) 在com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java: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) 在kotlinx.coroutines.test.internal.TestMainDispatcher.getDelegate(MainTestDispatcher.kt:19) 在kotlinx.coroutines.test.internal.TestMainDispatcher.getImmediate(MainTestDispatcher.kt:32) 在androidx.lifecycle.ViewModelKt.getViewModelScope(ViewModel.kt:42) 在app.topcafes.feed.viewmodel.FeedViewModel.bindIntents(FeedViewModel.kt:38) ...还有39个
答案 0 :(得分:6)
在生产中,因为使用了ViewModel的coroutineScopeProvider
,所以使用空viewModelScope
创建了ViewModel。为了进行测试,将TestCoroutineScope
作为ViewModel参数传递。
SomeUtils.kt
/**
* Configure CoroutineScope injection for production and testing.
*
* @receiver ViewModel provides viewModelScope for production
* @param coroutineScope null for production, injects TestCoroutineScope for unit tests
* @return CoroutineScope to launch coroutines on
*/
fun ViewModel.getViewModelScope(coroutineScope: CoroutineScope?) =
if (coroutineScope == null) this.viewModelScope
else coroutineScope
SomeViewModel.kt
class FeedViewModel(
private val coroutineScopeProvider: CoroutineScope? = null,
private val repository: FeedRepository
) : ViewModel() {
private val coroutineScope = getViewModelScope(coroutineScopeProvider)
fun getSomeData() {
repository.getSomeDataRequest().onEach {
// Some code here.
}.launchIn(coroutineScope)
}
}
SomeTest.kt
@ExperimentalCoroutinesApi
class FeedTest : 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 {
...
val viewModel = FeedViewModel(testScope, repository)
viewmodel.getSomeData()
...
}
}