单位测试Kotlin协程延迟

Eri*_*wne 27 unit-testing kotlin kotlinx.coroutines

我正在尝试对使用的Kotlin协程进行单元测试delay().对于单元测试我不关心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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

bj0*_*bj0 19

如果您不想要任何延迟,为什么不简单地在计划调用中恢复继续?:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

这种方式delay()将毫不拖延地恢复.请注意,这仍然会在延迟时暂停,因此其他协程仍然可以运行(如yield())

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

运行没有延迟和打印:

start
launched
stop
Run Code Online (Sandbox Code Playgroud)

编辑:

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

  • `Delay` 接口现在是 KotlinX 协程库中的一个内部 API。这个类,以及任何使用它的类,必须用 `@InternalCoroutinesApi` 注释标记。 (2认同)

Eri*_*wne 14

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


Eri*_*wne 5

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

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

缺点:如果你的协程代码不使用delay,并且你只是希望它在调用线程上同步执行,那么使用起来比TestUiContext@bj0 的答案稍微麻烦一些(你需要调用triggerActions()TestCoroutineContext 来让协程执行执行)。

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


Ada*_*itz 5

使用 TestCoroutineDispatcher、TestCoroutineScope 或 Delay

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

实施

在这种情况下,SomeViewModel的视图状态正在被测试。在该ERROR状态下,会发出一个错误值为 true 的视图状态。在定义的 Snackbar 时间长度过后,使用delay新的视图状态发出,错误值设置为 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)
}
Run Code Online (Sandbox Code Playgroud)

有很多方法可以处理delay. advanceUntilIdle很好,因为它不需要指定硬编码长度。此外,如果注入 TestCoroutineDispatcher,如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()
Run Code Online (Sandbox Code Playgroud)

这些也将起作用:

  • 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) at app.topcafes.FeedTest$topCafesTest$1.invoke(FeedTest.kt) at kotlinx.coroutines.test.TestBuildersKt$runBlockingTest$invokeTestBuilderspend. .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.start.AbstractCoroutine.start(AbstractCoroutine) kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:91) at kotlinx.coroutines.BuildersKt.async(Unknown Source) at kotlinx.coroutines.BuildersKt__Builders_commonKt.async$default(Builders:84lint).xt.kt.async coroutines.BuildersKt.async$default(Unknown Source) at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:49) at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:80) at app.topcafes。 FeedTest.topCafesTest(FeedTest.kt:41) 在 sun.reflect.NativeMethodAccessorImpl。invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at 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) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)在 org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) 在 org.junit.runners.BlockJUnit4ClassRunner。runChild(BlockJUnit4ClassRunner.java:57) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners .ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org. junit.runners.ParentRunner.run(ParentRunner.java:363) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) at 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”效果很好 (2认同)