Ada*_*itz 5 junit android kotlin kotlin-coroutines kotlin-flow
viewModelScope
使用 Kotlin 协程为 Android 单元测试注入的最佳策略是什么?
当 CoroutineScope 被注入到 ViewModel 进行单元测试时,flowOn
即使在生产代码中不需要CoroutineDispatcher 也应该注入和定义 using吗?
flowOn
在此用例中,生产代码中不需要,因为 RetrofitDispatchers.IO
在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)
}
Run Code Online (Sandbox Code Playgroud)
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)
}
}
Run Code Online (Sandbox Code Playgroud)
注意:最终版本中将使用 JUnit 5 测试扩展。
线程“main @coroutine#1”中的异常 java.lang.IllegalStateException: 带有 Main 调度程序的模块未能初始化。对于测试,来自 kotlinx-coroutines-test 模块的 Dispatchers.setMain 可用于 kotlinx.coroutines.internal.MissingMainCoroutineDispatcher.missing(MainDispatchers.kt:113) at kotlinx.coroutines.internal.MissingMainCoroutineDispatcher.isDispatchNespatcher.isDispatchNeeds(at) kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:285) at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) at kotlinx.coroutines.CoroutineStart.invoke)atcoroutineStart.invoke) at kotlinx.coroutines.intrinsics. .AbstractCoroutine.start(AbstractCoroutine.kt:158) 在 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.getDispatcher. 32) 在 androidx.lifecycle.ViewModelKt.getViewModelScope(ViewModel.kt:42) ... 线程“main @coroutine#1”中还有 40 个异常 java.lang.IllegalStateException:带有主调度程序的模块未能初始化。对于测试调度员。prepareStreamsAndStart(JUnitStarter.java:230) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58) 由:java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked。看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.getDispatcher. 32) 在 androidx.lifecycle.ViewModelKt.getViewModelScope(ViewModel.kt:42) 在 app.topcafes.feed.viewmodel.FeedViewModel.bindIntents(FeedViewModel.kt:38) ... 39 更多
在生产中,ViewModel 是用 null 创建的coroutineScopeProvider
,因为使用了 ViewModel viewModelScope
。对于测试,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
Run Code Online (Sandbox Code Playgroud)
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)
}
}
Run Code Online (Sandbox Code Playgroud)
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()
...
}
}
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
3385 次 |
最近记录: |