问题描述
-
使用Kotlin协程为Android单元测试注入
viewModelScope
的最佳策略是什么?
将CoroutineScope注入ViewModel进行单元测试时,即使生产代码中不需要使用flowOn
,也应该使用CoroutineDispatcher进行注入和定义吗?
When the CoroutineScope is injected into a ViewModel for unit tests, should the CoroutineDispatcher also be injected and defined using flowOn
even if it is not needed in production code?
在此用例中,生产代码中不需要
flowOn
,因为Retrofit处理 SomeRepository.kt 中Dispatchers.IO
上的线程,而viewModelScope
返回Dispathers.Main
上的数据,默认情况下都是如此.
flowOn
is not needed in the production code in this use case as Retrofit handles the threading on Dispatchers.IO
in SomeRepository.kt, and the viewModelScope
returns the data on Dispathers.Main
, both by default.
对保存在Kotlin Flow值中的Android ViewModel视图状态值进行单元测试.
Run a unit test on Android's ViewModel view state values saved in a Kotlin Flow value.
在第一次对CoroutineScope进行硬编码的情况下,单元测试失败.使用viewModelScope
以便启动的协程将维持ViewModel的生命周期.但是,viewModelScope
是在ViewModel内部创建的,与可在ViewModel外部定义并作为参数传递的CoroutineDispatcher相比,它的注入更加复杂.
The unit test is failing on the first occurrence where a CoroutineScope is hardcoded. viewModelScope
is utilized so that the coroutine launched will maintain the lifecycle of the ViewModel. However, viewModelScope
is created from within the ViewModel, which makes it more complicated to inject compared to a CoroutineDispatcher that can be defined outside the ViewModel and passed in as an argument.
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测试扩展.
Note: A JUnit 5 test extension will be used in the final version.
推荐答案
在创建ViewModel时注入并确定CoroutineScope
在生产中,将使用null coroutineScopeProvider
创建ViewModel,因为使用了ViewModel的viewModelScope
.为了进行测试,将TestCoroutineScope
作为ViewModel参数传递.
Inject and determine CoroutineScope on ViewModel creation
In production, the ViewModel is created with a null coroutineScopeProvider
, as the ViewModel's viewModelScope
is used. For testing, TestCoroutineScope
is passed as the ViewModel argument.
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()
...
}
}
这篇关于如何使用Kotlin协程注入viewModelScope for Android单元测试?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!