Unit testing 如何对Kotlin挂起函数进行单元测试

Unit testing 如何对Kotlin挂起函数进行单元测试,unit-testing,kotlin,mockito,mvp,coroutine,Unit Testing,Kotlin,Mockito,Mvp,Coroutine,我遵循MVP模式+用例与模型层交互。这是我要测试的演示者中的一个方法: fun loadPreviews() { launch(UI) { val items = previewsUseCase.getPreviews() // a suspending function println("[method] UseCase items: $items") println("[method] View call") view

我遵循MVP模式+用例与模型层交互。这是我要测试的演示者中的一个方法:

fun loadPreviews() {
    launch(UI) {
        val items = previewsUseCase.getPreviews() // a suspending function
        println("[method] UseCase items: $items")

        println("[method] View call")
        view.showPreviews(items)
    }
}
我的简单BDD测试:

fun <T> givenSuspended(block: suspend () -> T) = BDDMockito.given(runBlocking { block() })

infix fun <T> BDDMockito.BDDMyOngoingStubbing<T>.willReturn(block: () -> T) = willReturn(block())

@Test
fun `load previews`() {
    // UseCase and View are mocked in a `setUp` method

    val items = listOf<PreviewItem>()
    givenSuspended { previewsUseCase.getPreviews() } willReturn { items }

    println("[test] before Presenter call")
    runBlocking { presenter.loadPreviews() }
    println("[test] after Presenter call")

    println("[test] verify the View")
    verify(view).showPreviews(items)
}
fun givenSuspended(block:suspend()->T)=BDDMockito.given(runBlocking{block()})
infix fun BDDMockito.bddmyongoingstubing.willReturn(block:()->T)=willReturn(block())
@试验
有趣的“加载预览”(){
//用例和视图在“setUp”方法中模拟
val items=listOf()
givenSuspended{PreviewUseCase.getPreviews()}将返回{items}
println(“[测试]在演示者调用之前”)
runBlocking{presenter.loadPreviews()}
println(“演示者呼叫后的[测试])
println(“[测试]验证视图”)
验证(查看)。显示预览(项目)
}
测试成功通过,但日志中有一些奇怪的东西。我希望是:

  • “[测试]演示者呼叫前”
  • “[方法]用例项:[]”
  • “[方法]视图调用”
  • “[测试]演示者呼叫后”
  • “[测试]验证视图”
但事实证明:

  • [测试]演示者呼叫前
  • [测试]演示者呼叫后
  • [测试]验证视图
  • [方法]用例项:[]
  • [方法]查看调用

这种行为的原因是什么?我应该如何修复它?

我发现这是因为一个
CoroutineDispatcher
。我曾经用
EmptyCoroutineContext
模拟
UI
上下文。切换到
Unconfined
解决了问题

更新02.04.20 问题的名称表明,将有一个详尽的解释如何单元测试一个挂起的函数。让我再解释一下

测试挂起函数的主要问题是线程。假设我们想测试这个简单的函数,它在不同的线程中更新属性的值:

类项目更新程序(val-item:item){
fun updateItemValue(){
启动(Dispatchers.Default){item.value=42}
}
}
我们需要用另一个调度器替换
调度器.Default
,仅用于测试目的。我们有两种方法可以做到这一点。每种方法都有其优缺点,选择哪一种取决于您的项目和编码风格:

1。注入调度程序

类项目更新程序(
val项目:项目,
val dispatcher:coroutinedSpatcher//可以是一个包装器,它提供多个调度程序,但让我们保持它简单
) {
fun updateItemValue(){
启动(调度程序){item.value=42}
}
}
//后来在一个测试班
@试验
fun`item值已更新`()=runBlocking{
val项目=项目()
val testDispatcher=Dispatchers.Unconfined//可以是TestCoroutineDispatcher,但我们仍然保持简单
val updater=ItemUpdater(项,testDispatcher)
updater.updateItemValue()
资产质量(42,项目价值)
}
2。替换调度员。

类项目更新程序(val-item:item){
fun updateItemValue(){
launch(DispatchersProvider.Default){item.value=42}//DispatchersProvider是我们自己的全局包装器
}
}
//后来在一个测试班
// -----------------------------------------------------------------------------------
//---此块可以提取到JUnit规则中,并替换为一行---
// -----------------------------------------------------------------------------------
@以前
趣味设置(){
DispatchersProvider.Default=Dispatchers.Unconfined
}
@之后
趣味大扫除(){
DispatchersProvider.Default=Dispatchers.Default
}
// -----------------------------------------------------------------------------------
@试验
fun`item值已更新`()=runBlocking{
val项目=项目()
val updater=ItemUpdater(项目)
updater.updateItemValue()
资产质量(42,项目价值)
}
它们都在做同样的事情——它们替换了测试类中原来的
分派器。唯一的区别是他们是如何做到的。这真的取决于你选择哪一个,所以不要被下面我自己的想法所偏袒


IMHO:第一种方法有点太麻烦了。到处注入dispatchers将导致使用额外的
DispatchersWrapper
来污染大多数类的构造函数,而这仅仅是出于测试目的。不过谷歌至少目前是这样。第二种样式保持简单,不会使生产类复杂化。这就像RxJava的测试方式,您必须通过RxJava插件替换调度器。顺便说一句,
kotlinx coroutines测试
将来的某一天。

我看到你自己发现了,但我想为可能遇到同样问题的人解释更多

当您执行
launch(UI){}
时,将创建一个新的协同路由并将其分派给“UI”调度程序,这意味着您的协同路由现在运行在不同的线程上

您的
runBlocking{}
调用创建一个新的协同程序,但是
runBlocking{}
将等待此协同程序结束,然后继续,您的
loadPreviews()
函数创建一个协同程序,启动它,然后立即返回,所以
runBlocking()
只需等待它并返回即可

因此,当
runBlocking{}
返回时,您使用
launch(UI){}
创建的协同程序仍在另一个线程中运行,这就是日志顺序混乱的原因

Unconfined
上下文是一个特殊的
CoroutineContext
,它只是创建一个调度器,在当前线程上执行coroutine,因此现在当您执行
runBlocking{}
时,它必须等待
launch{}创建的coroutine
结束,因为它在同一线程上运行,因此阻塞了该线程


我希望我的解释是清楚的,祝你过得愉快

建议你改为加入coroutine scope&dispatchers:我不喜欢污染电动汽车的想法