【问题标题】:Unable to assert state flow value in view model无法在视图模型中断言状态流值
【发布时间】:2023-02-19 20:28:09
【问题描述】:

视图模型如下

class ClickRowViewModel @Inject constructor(
    private val clickRowRepository: ClickRowRepository
): ViewModel() {

private val _clickRowsFlow = MutableStateFlow<List<ClickRow>>(mutableListOf())
val clickRowsFlow = _clickRowsFlow.asStateFlow()

fun fetchAndInitialiseClickRows() {
    viewModelScope.launch {
        _clickRowsFlow.update {
            clickRowRepository.fetchClickRows()
        }
    }
}

}

我的测试如下:

我正在使用 InstantTaskExecutorRule 如下

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

实际值永远不会解析为预期值,即使 $result 似乎有两个元素但 actualValue 是一个空列表。我不知道我做错了什么。

更新

我尝试使用 first 终端操作员以及 但返回的输出返回一个空列表。

更新#2

我试过 async 但我收到以下错误

kotlinx.coroutines.test.UncompletedCoroutinesError: After waiting for 60000 ms, the test coroutine is not completing, there were active child jobs: [DeferredCoroutine{Active}@a4a38f0]
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine$3$3.invokeSuspend(TestBuilders.kt:342)

更新 #3

此测试在 Android Studio 中通过,但使用 CLI 失败

CLI 中测试失败

【问题讨论】:

    标签: kotlin kotlin-coroutines kotlin-flow


    【解决方案1】:

    你不能像这样在 SharedFlow 上调用 toList

    共享流程永远不会完成。在共享流上对 Flow.collect 的调用永远不会正常完成,由 Flow.launchIn 函数启动的协程也不会正常完成。

    所以调用 toList 将永远挂起,因为流程永远不会到达它说“好的,这就是所有元素”的终点,并且 toList 需要返回最终值。由于 StateFlow 一次只包含一个元素,而且您不会在一段时间内收集,因此您可能只需要 take(1).toList()

    或者,如果您不想要包装列表,则使用 first(),您似乎不需要包装列表 - StateFlow 中的每个元素都是一个 List&lt;ClickRow&gt;,这也是 clickRowRepository.fetchClickRows() 返回的内容。所以 expectedValueList&lt;ClickRow&gt;,而 actualValueList&lt;List&lt;ClickRow&gt;&gt; - 所以它们无论如何都不匹配!


    编辑您的更新(使用first())有几个问题。

    首先,您的 ViewModel 中的 clickRowsFlow StateFlow仅有的当您调用 fetchAndInitialiseClickRows() 时更新,因为这是获取值并将其设置在 StateFlow 上的内容。您没有在第二个示例中调用它,因此它不会更新。

    其次,StateFlow 将经历两个状态值,对吗?第一个是初始空列表,第二个是您从 repo 返回的行内容。所以当你访问那个StateFlow时,它要么需要更新已经发生,或者(更好)你需要忽略第一的声明并只返回第二一:

    val actualValue = clickRowViewModel.clickRowsFlow
        .drop(1) // ignore the initial state
        .first() // then take the first result after that
    
    // start the update -after- setting up the flow collection,
    // so there's no race condition to worry about
    clickRowsViewModel.fetchAndInitialiseClickRows()
    

    这样,您订阅 StateFlow 并立即获得(和删除)初始状态。然后当更新发生时,它应该将另一个值推送给订阅者,订阅者将 first 新值作为其最终结果。


    但是还有另一个并发症 - 因为 fetchAndInitialiseClickRows() 开始它自己的协程并立即返回,这意味着获取和更新任务正在异步运行。在开始声明任何结果之前,您需要给它时间来完成。

    一种选择是启动协同程序,然后阻塞等待结果显示:

    // start the update
    clickRowsViewModel.fetchAndInitialiseClickRows()
    
    // run the collection as a blocking operation, which completes when you get
    // that second result
    val actualValue = clickRowViewModel.clickRowsFlow
        .drop(1)
        .first()
    

    这有效只要fetchAndInitialiseClickRows 没有立即完成.上面那个消费链需要至少要生产两个项目当它被订阅时- 如果它永远无法看到初始状态,它将挂起等待永远不会出现的第二个(实际上是第三个)值。这引入了竞争条件,即使它“在实践中可能很好”,它仍然会使测试变得脆弱。

    您的另一个选择是先订阅,使用协程以便可以继续执行,并且然后开始更新 - 这样订阅者可以看到初始状态,然后是稍后到达的更新:

    // async is like launch, but it returns a `Deferred` that produces a result later
    val actualValue = async {
        clickRowViewModel.clickRowsFlow
            .drop(1)
            .first()
    }
    
    // now you can start the update
    clickRowsViewModel.fetchAndInitialiseClickRows()
    
    // then use `await` to block until the result is available
    assertEquals(expected, actualValue.await())
    

    总是需要确保你处理等待协程,否则测试可能会提前完成(即你在结果出现之前进行断言)。就像在您的第一个示例中一样,您正在启动一个协程来填充您的列表,但不确保在检查列表的内容之前有时间完成。

    在那种情况下,你必须做类似advanceUntilIdle() - have a look at this section on testing coroutines 的事情,它会告诉你一些等待他们的方法。这也可能适用于您使用 fetchAndInitialiseClickRows 启动的那个(因为它说它等待调度程序上的其他协程,而不是相同的范围)但我不太熟悉它,如果您可以查看它喜欢!

    【讨论】:

    • 我尝试使用 toList 运算符,但它填充了一个空列表列表,因此它对我不起作用。
    • @Kartik 我猜你的意思是你试过 first() 因为那是你在问题中编辑的内容 - 我已经添加到我的答案中因为有相当多的解释
    • 我试过你的方法,但我的测试失败了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-07-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多