如何单元测试 Kotlin 挂起函数



我遵循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)
}

测试成功通过,但日志中有一些奇怪的东西。我希望它是:

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

但事实证明:

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

这种行为的原因是什么,我应该如何解决它?

我发现这是因为CoroutineDispatcher.我曾经用EmptyCoroutineContext嘲笑UI上下文.切换到Unconfined解决了问题

更新 02.04.20

问题的名称表明将有一个详尽的解释如何对挂起函数进行单元测试。因此,让我再解释一下。

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

class ItemUpdater(val item: Item) {
fun updateItemValue() {
launch(Dispatchers.Default) { item.value = 42 }
}
}

我们需要以某种方式将Dispatchers.Default替换为另一个调度程序,仅用于测试目的。我们有两种方法可以做到这一点。每个都有其优点和缺点,选择哪一个取决于您的项目和编码风格:

1. 注入调度程序

class ItemUpdater(
val item: Item,
val dispatcher: CoroutineDispatcher  // can be a wrapper that provides multiple dispatchers but let's keep it simple
) {
fun updateItemValue() {
launch(dispatcher) { item.value = 42 }
}
}
// later in a test class
@Test
fun `item value is updated`() = runBlocking {
val item = Item()
val testDispatcher = Dispatchers.Unconfined   // can be a TestCoroutineDispatcher but we still keep it simple
val updater = ItemUpdater(item, testDispatcher)
updater.updateItemValue()
assertEquals(42, item.value)
}

2. 替换调度员。

class ItemUpdater(val item: Item) {
fun updateItemValue() {
launch(DispatchersProvider.Default) { item.value = 42 }  // DispatchersProvider is our own global wrapper
}
}
// later in a test class
// -----------------------------------------------------------------------------------
// --- This block can be extracted into a JUnit Rule and replaced by a single line ---
// -----------------------------------------------------------------------------------
@Before
fun setUp() {
DispatchersProvider.Default = Dispatchers.Unconfined
}
@After
fun cleanUp() {
DispatchersProvider.Default = Dispatchers.Default
}
// -----------------------------------------------------------------------------------
@Test
fun `item value is updated`() = runBlocking {
val item = Item()
val updater = ItemUpdater(item)
updater.updateItemValue()
assertEquals(42, item.value)
}

它们都在做同样的事情 - 它们替换了测试类中的原始Dispatchers.Default。唯一的区别是他们是如何做到这一点的。这真的取决于你选择他们中的哪一个,所以不要被我自己的想法所偏见。

恕我直言:第一种方法有点太麻烦了。在任何地方注入调度程序将导致污染大多数类的构造函数,并带有仅用于测试目的的额外DispatchersWrapper。然而,谷歌至少目前推荐这种方式。第二种风格使事情变得简单,并且不会使生产类复杂化。这就像RxJava的测试方式,你必须通过RxJava插件替换调度程序。顺便说一下,kotlinx-coroutines-test将来有一天会带来完全相同的功能。

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

当您执行launch(UI) {}时,将创建一个新的协程并将其调度到"UI"调度程序,这意味着您的协程现在在不同的线程上运行。

您的runBlocking{}调用会创建一个新的协程,但runBlocking{}会等待此协程结束再继续,您的loadPreviews()函数会创建一个协程,启动它,然后立即返回,因此runBlocking()等待它并返回。

因此,虽然runBlocking{}已返回,但您使用launch(UI){}创建的协程仍在不同的线程中运行,这就是日志顺序混乱的原因

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

我希望我的解释很清楚,祝你有美好的一天

相关内容

  • 没有找到相关文章

最新更新