如何测试在断言之前阻塞线程的 View Model 协程?
Posted
技术标签:
【中文标题】如何测试在断言之前阻塞线程的 View Model 协程?【英文标题】:How to test View Model coroutines that blocks the thread before assertions? 【发布时间】:2022-01-08 15:14:44 【问题描述】:我正在尝试为视图模型编写测试。因为我需要context
,所以我正在进行仪器测试。
视图模型和测试如下:
class MyViewModel(
private val dispatcher: CoroutineDispatchers = Dispatchers.IO) : ViewModel()
private val _livedata = MutableLiveData<Boolean>()
val livedata: LiveData<Boolean> = _livedata
fun doSomething()
viewModelScope.launch(dispatcher)
//suspend function with retrofit
_livedata.value = true
class MyViewModelTest
private lateinit var viewModel: MyViewModel
@get:Rule
var mainCoroutineRule = MainCoroutineRule()
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun setup()
viewModel = MyViewModel(mainCoroutineRule.dispatcher)
@Test
fun testMyViewModel()
mainCoroutineRule.runBlockingTest
viewModel.doSomething()
mainCoroutineRule.dispatcher.advanceUntilIdle()
val result = viewModel.livedata.getOrAwaitValue()
assertThat(result).isTrue()
问题是result
是如何为空的,因为doSomething()
在另一个协程上被调用并且是异步完成的。
如何运行我的测试,以便挂起函数阻塞线程,以便我的断言在挂起函数完成后捕获result
?
我对那里的信息感到很困惑。
我认为我不需要 InstantTaskExecutorRule()
,因为我正在进行仪器测试?
添加此规则没有帮助:
@ExperimentalCoroutinesApi
class MainCoroutineRule(val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()):
TestWatcher(),
TestCoroutineScope by TestCoroutineScope(dispatcher)
override fun starting(description: Description?)
super.starting(description)
Dispatchers.setMain(dispatcher)
override fun finished(description: Description?)
super.finished(description)
cleanupTestCoroutines()
Dispatchers.resetMain()
我是否需要在我的视图模型中注入一个阻塞主线程的协程调度程序?
【问题讨论】:
你真的试过InstantTaskExecutorRule
吗?您是否实际上添加了MainCoroutineRule
的实例,或者您只是按照您的问题所示声明它?发布完整的示例。
我已经用我目前的内容更新了这篇文章。挂起函数正在调用改造 api 调用,上下文/调度程序中没有其他开关。然后它最终在实时数据上调用postValue
。
我会尝试添加日志并打印出当前的协程/线程值以查看顺序。
我发现当我在我的函数中记录线程名称时,它都使用Instr: androidx.test.runner.AndroidJUnitRunner
。这会是正确的线程吗?
【参考方案1】:
如果您要启动协程来调用 Retrofit 服务,您可能希望在 IO 调度程序上执行此操作。您可以将其作为依赖项传入,然后在测试中通过一个测试来控制它。
class MyViewModel : ViewModel(private val ioDispatcher: Dispatcher = Dispatchers.IO)
private val _livedata = MutableLiveData<Boolean>()
val livedata: LiveData<Boolean> = _livedata
fun doSomething()
viewModelScope.launch(ioDispatcher)
//suspend function with retrofit
_livedata.value = true
测试:
@Before
fun setup()
viewModel = MyViewModel(coroutineRule.dispatcher)
@Test
fun testMyViewModel()
mainCoroutineRule.runBlockingTest
viewModel.doSomething()
coroutineRule.dispatcher.advanceUntilIdle()
val result = viewModel.livedata.value
assertThat(result).isTrue()
【讨论】:
这不起作用。runBlockingTest
也不起作用。我认为它只有在 doSomething()
本身是一个挂起函数时才有效。
我发布了另一个建议。
我在这篇文章中找到了getOrAwait
来解决问题medium.com/androiddevelopers/…。为了测试实时数据,您似乎需要一个观察者。
嗯 - 该示例有一个依赖的 livedata 对象。您的示例实际上是设置实时数据内部字段,然后对其进行检查。它不应该被观察。但如果它有效... :shrug:
其实你是对的。它不起作用。如果 API 调用在 2 秒内完成,getOrAwaitValue
就会起作用,这样它就不会引发异常。但是挂起函数仍然是异步运行并且没有阻塞断言的问题。【参考方案2】:
为了解决这个问题,我不得不使用https://medium.com/androiddevelopers/unit-testing-livedata-and-other-common-observability-problems-bb477262eb04的这个辅助函数
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS
): T
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T>
override fun onChanged(o: T?)
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
this.observeForever(observer)
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit))
throw TimeoutException("LiveData value was never set.")
@Suppress("UNCHECKED_CAST")
return data as T
【讨论】:
以上是关于如何测试在断言之前阻塞线程的 View Model 协程?的主要内容,如果未能解决你的问题,请参考以下文章