Kotlin Flow:测试挂起

vrg*_*grg 2 kotlin kotlin-coroutines kotlin-flow kotest kotlin-coroutines-flow

我正在尝试使用 Flows 测试 Kotlin 实现。我使用 Kotest 进行测试。此代码有效:

视图模型:

val detectedFlow = flow<String> {
    emit("123")
    delay(10L)
    emit("123")
}
Run Code Online (Sandbox Code Playgroud)

测试:

class ScanViewModelTest : StringSpec({
    "when the flow contains values they are emitted" {
        val detectedString = "123"
        val vm = ScanViewModel()
        launch {
            vm.detectedFlow.collect {
                it shouldBe detectedString
            }
        }
    }
})
Run Code Online (Sandbox Code Playgroud)

但是,在真正的 ViewModel 中我需要向流添加值,所以我使用ConflatedBroadcastChannel如下:

private val _detectedValues = ConflatedBroadcastChannel<String>()
val detectedFlow = _detectedValues.asFlow()

suspend fun sendDetectedValue(detectedString: String) {
    _detectedValues.send(detectedString)
}
Run Code Online (Sandbox Code Playgroud)

然后在测试中我尝试:

"when the flow contains values they are emitted" {
    val detectedString = "123"
    val vm = ScanViewModel()
    runBlocking {
        vm.sendDetectedValue(detectedString)
    }
    runBlocking {
        vm.detectedFlow.collect { it shouldBe detectedString }
    }
}
Run Code Online (Sandbox Code Playgroud)

测试只是挂起,永远不会完成。我尝试了各种各样的事情:launch或者runBlockingTest不是runBlocking,将发送和收集放在相同或单独的协程中,offer而不是send......似乎没有什么可以解决它。我究竟做错了什么?

更新:如果我手动创建流它的工作原理:

private val _detectedValues = ConflatedBroadcastChannel<String>()
val detectedFlow =  flow {
    this.emit(_detectedValues.openSubscription().receive())
}
Run Code Online (Sandbox Code Playgroud)

那么,这是asFlow()方法上的错误吗?

小智 8

问题是您在测试中使用的collect函数是一个挂起函数,它将挂起执行直到Flow完成。

在第一个例子中,你detectedFlow是有限的。它只会发出两个值并完成。在您的问题更新中,您还将创建一个有限流,它将发出一个值并完成。这就是您的测试有效的原因。

但是,在第二个(现实生活中的)示例中,流是从ConflatedBroadcastChannel 从未关闭的a 创建的。因此该collect函数永远挂起执行。为了使测试工作而不会永远阻塞线程,您还需要使流程有限。我通常first()为此使用运算符。另一种选择是closeConflatedBroadcastChannel,但是这通常意味着修改您的代码只是因为测试是不是一个好的做法。

这是您的测试与first()操作员一起工作的方式

"when the flow contains values they are emitted" {
    val detectedString = "123"
    val vm = ScanViewModel()
    runBlocking {
        vm.sendDetectedValue(detectedString)
    }
    runBlocking {
        vm.detectedFlow.first() shouldBe detectedString
    }
}
Run Code Online (Sandbox Code Playgroud)