如何测试在断言之前阻塞线程的 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 协程?的主要内容,如果未能解决你的问题,请参考以下文章

在 Python 中测试另一个线程的结果时,当断言失败时,PyTest 测试套件是不是通过?

jmeter http请求+线程组+事务+断言

技术分享 | 接口自动化测试如何搞定 json 响应断言?

(二)jmeter完成一个简单接口测试和断言

如何追踪阻塞主(UI)线程的调用?

在 Spring Boot 中,如何在每次测试之前重置指标注册表?