Unit testing 如何对Kotlin挂起函数进行单元测试
我遵循MVP模式+用例与模型层交互。这是我要测试的演示者中的一个方法: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
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:我不喜欢污染电动汽车的想法