【问题标题】:Unit testing a Kotlin coroutine with delay延迟对 Kotlin 协程进行单元测试
【发布时间】:2018-04-20 15:27:28
【问题描述】:

我正在尝试对使用 delay() 的 Kotlin 协程进行单元测试。对于单元测试,我不关心delay(),它只是减慢了测试速度。我想以某种在调用delay() 时实际上不会延迟的方式运行测试。

我尝试使用委托给 CommonPool 的自定义上下文运行协程:

class TestUiContext : CoroutineDispatcher(), Delay {
    suspend override fun delay(time: Long, unit: TimeUnit) {
        // I'd like it to call this
    }

    override fun scheduleResumeAfterDelay(time: Long, unit: TimeUnit, continuation: CancellableContinuation<Unit>) {
        // but instead it calls this
    }

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        CommonPool.dispatch(context, block)
    }
}

我希望我可以从上下文的 delay() 方法返回,但它却调用了我的 scheduleResumeAfterDelay() 方法,我不知道如何将其委托给默认调度程序。

【问题讨论】:

  • 你不能把延迟超时设置成可配置的,这样在你的测试中你可以选择一个很小的吗?!
  • @s1m0nw1 是的,但我宁愿有一个通用的解决方案,而不必像那样修改我的代码。
  • @eoinmullan,你能分享一个最小的 git 项目吗?

标签: android unit-testing kotlin android-testing kotlin-coroutines


【解决方案1】:

如果您不希望有任何延迟,为什么不简单地在调度调用中恢复继续?:

class TestUiContext : CoroutineDispatcher(), Delay {
    override fun scheduleResumeAfterDelay(time: Long, unit: TimeUnit, continuation: CancellableContinuation<Unit>) {
        continuation.resume(Unit)
    }

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        //CommonPool.dispatch(context, block)  // dispatch on CommonPool
        block.run()  // dispatch on calling thread
    }
}

这样delay() 将立即恢复。请注意,这仍然会延迟暂停,因此其他协程仍然可以运行(例如yield()

@Test
fun `test with delay`() {
    runBlocking(TestUiContext()) {
        launch { println("launched") }
        println("start")
        delay(5000)
        println("stop")
    }
}

无延迟运行并打印:

start
launched
stop

编辑:

您可以通过自定义dispatch 函数来控制继续运行的位置。

【讨论】:

  • dispatch() 可以只是block.run(),它会在主线程中保持执行。
  • 是的,这可能对测试有意义,尽管他似乎希望它使用 CommonPool。
  • 我是 OP,我宁愿它使用相同的线程。做出改变,我会接受你的回答。
  • Delay 接口现在是 KotlinX 协程库中的内部 API。这个类,以及任何使用它的类,都必须用@InternalCoroutinesApi 注解来标记。
【解决方案2】:

在 kotlinx.coroutines v1.2.1 中,他们添加了 kotlinx-coroutines-test 模块。它包括runBlockingTest 协程构建器,以及TestCoroutineScopeTestCoroutineDispatcher。它们允许自动推进时间,以及使用delay 显式控制测试协程的时间。

【讨论】:

  • 大量资源共享@Erik Browne!具体来说,您将如何实现runBlockingTestTestCoroutineScopeTestCoroutineDispatcher 来处理单元测试中的delay
  • 我已经扩展了这个解决方案,具体实现here
  • 在 kotlinx.coroutines v1.6.0 中,他们改进了测试库,将 TestCoroutineScopeTestCoroutineDispatcher 替换为 TestScopeTestDispatcher
【解决方案3】:

使用 TestCoroutineDispatcher、TestCoroutineScope 或 Delay

TestCoroutineDispatcher、TestCoroutineScope 或 Delay 可用于处理在测试的生产代码中制作的 Kotlin 协程中的 delay

实施

在这种情况下,SomeViewModel 的视图状态正在测试中。在ERROR 状态下,会发出一个视图状态,错误值为真。在使用delay 定义的 Snackbar 时间长度过去后,会发出一个新的视图状态,并将错误值设置为 false。

SomeViewModel.kt

private fun loadNetwork() {
    repository.getData(...).onEach {
        when (it.status) {
            LOADING -> ...
            SUCCESS ...
            ERROR -> {
                _viewState.value = FeedViewState.SomeFeedViewState(
                    isLoading = false,
                    feed = it.data,
                    isError = true
                )
                delay(SNACKBAR_LENGTH)
                _viewState.value = FeedViewState.SomeFeedViewState(
                    isLoading = false,
                    feed = it.data,
                    isError = false
                )
            }
        }
    }.launchIn(coroutineScope)
}

有很多方法可以处理delayadvanceUntilIdle 很好,因为它不需要指定硬编码的长度。此外,如果注入 TestCoroutineDispatcher,如 outlined by Craig Russell,这将由 ViewModel 内部使用的同一调度程序处理。

SomeTest.kt

private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)

// Code that initiates the ViewModel emission of the view state(s) here.

testDispatcher.advanceUntilIdle()

这些也可以:

  • testScope.advanceUntilIdle()
  • testDispatcher.delay(SNACKBAR_LENGTH)
  • delay(SNACKBAR_LENGTH)
  • testDispatcher.resumeDispatcher()
  • testScope.resumeDispatcher()
  • testDispatcher.advanceTimeBy(SNACKBAR_LENGTH)
  • testScope.advanceTimeBy(SNACKBAR_LENGTH)

没有处理延迟的错误

kotlinx.coroutines.test.UncompletedCoroutinesError:拆卸期间未完成的协程。确保您的测试完成或取消所有协程。

在 kotlinx.coroutines.test.TestCoroutineDispatcher.cleanupTestCoroutines(TestCoroutineDispatcher.kt:178) 在 app.topcafes.FeedTest.cleanUpTest(FeedTest.kt:127) 在 app.topcafes.FeedTest.access$cleanUpTest(FeedTest.kt:28) 在 app.topcafes.FeedTest$topCafesTest$1.invokeSuspend(FeedTest.kt:106) 在 app.topcafes.FeedTest$topCafesTest$1.invoke(FeedTest.kt) 在 kotlinx.coroutines.test.TestBuildersKt$runBlockingTest$deferred$1.invokeSuspend(TestBuilders.kt:50) 在 kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) 在 kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56) 在 kotlinx.coroutines.test.TestCoroutineDispatcher.dispatch(TestCoroutineDispatcher.kt:50) 在 kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:288) 在 kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) 在 kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109) 在 kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:158) 在 kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:91) 在 kotlinx.coroutines.BuildersKt.async(未知来源) 在 kotlinx.coroutines.BuildersKt__Builders_commonKt.async$default(Builders.common.kt:84) 在 kotlinx.coroutines.BuildersKt.async$default(未知来源) 在 kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:49) 在 kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:80) 在 app.topcafes.FeedTest.topCafesTest(FeedTest.kt:41) 在 sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 在 sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 在 sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 在 java.lang.reflect.Method.invoke(Method.java:498) 在 org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) 在 org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) 在 org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) 在 org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) 在 org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) 在 org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) 在 org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57) 在 org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) 在 org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) 在 org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) 在 org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) 在 org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) 在 org.junit.runners.ParentRunner.run(ParentRunner.java:363) 在 org.junit.runner.JUnitCore.run(JUnitCore.java:137) 在 com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) 在 com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33) 在 com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230) 在 com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

【讨论】:

  • 如果我使用testDispatcher.advanceTimeBy,我会收到未完成的协程错误,我不确定如何解决。 testDispatcher.advanceUntilIdle 虽然效果很好
  • 很高兴听到 advanceUntilIdle 为您完成这项工作。在TestCoroutineDispatcher().runBlockingTest{...} 中运行测试可能会将未完成的协程错误解决为outlined by Craig Russell here
【解决方案4】:

在 kotlinx.coroutines v0.23.0 中,他们引入了TestCoroutineContext

Pro:它使真正使用 delay 测试协程成为可能。您可以将 CoroutineContext 的虚拟时钟设置为某个时刻并验证预期的行为。

Con:如果您的协程代码不使用delay,而您只是希望它在调用线程上同步执行,那么使用它比@bj0 的答案中的TestUiContext 稍微麻烦一些(您需要在 TestCoroutineContext 上调用 triggerActions() 以获取要执行的协程。

旁注: TestCoroutineContext 现在存在于从协程 1.2.1 版本开始的 kotlinx-coroutines-test 模块中,并且在此版本以上的版本中将被标记为已弃用或不存在于标准协程库中。

【讨论】:

  • 快速更新 TestCoroutineContext 已在更高版本的协程库中被弃用。
  • 在 v1.2.1 中他们添加了实验性的kotlinx-coroutines-test 模块,包括runBlockingTestTestCoroutineScopeTestCoroutineDispatcher
猜你喜欢
  • 1970-01-01
  • 2019-04-15
  • 2018-05-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-13
  • 1970-01-01
  • 2019-01-15
相关资源
最近更新 更多