简体   繁体   English

延迟对 Kotlin 协程进行单元测试

[英]Unit testing a Kotlin coroutine with delay

I'm trying to unit test a Kotlin coroutine that uses delay() .我正在尝试对使用delay()的 Kotlin 协程进行单元测试。 For the unit test I don't care about the delay() , it's just slowing the test down.对于单元测试,我不关心delay() ,它只是减慢了测试速度。 I'd like to run the test in some way that doesn't actually delay when delay() is called.我想以某种方式运行测试,当调用delay()时实际上不会延迟。

I tried running the coroutine using a custom context which delegates to CommonPool:我尝试使用委托给 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)
    }
}

I was hoping I could just return from my context's delay() method, but instead it's calling my scheduleResumeAfterDelay() method, and I don't know how to delegate that to the default scheduler.我希望我可以从我的上下文的delay()方法返回,但它正在调用我的scheduleResumeAfterDelay()方法,我不知道如何将它委托给默认的调度程序。

If you don't want any delay, why don't you simply resume the continuation in the schedule call?:如果您不想有任何延迟,为什么不简单地在 schedule 调用中恢复延续?:

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
    }
}

That way delay() will resume with no delay.这样delay()将立即恢复。 Note that this still suspends at delay, so other coroutines can still run (like yield() )请注意,这仍然会延迟挂起,因此其他协程仍然可以运行(例如yield()

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

Runs without delay and prints:立即运行并打印:

start
launched
stop

EDIT:编辑:

You can control where the continuation is run by customizing the dispatch function.您可以通过自定义dispatch函数来控制在何处运行延续。

In kotlinx.coroutines v1.2.1 they added the kotlinx-coroutines-test module.在 kotlinx.coroutines v1.2.1 中,他们添加了kotlinx-coroutines-test模块。 It includes the runBlockingTest coroutine builder, as well as a TestCoroutineScope and TestCoroutineDispatcher .它包括runBlockingTest协程构建器,以及TestCoroutineScopeTestCoroutineDispatcher They allow auto-advancing time, as well as explicitly controlling time for testing coroutines with delay .它们允许自动推进时间,以及使用delay显式控制测试协程的delay

In kotlinx.coroutines v0.23.0 they introduced a TestCoroutineContext .在 kotlinx.coroutines v0.23.0 中,他们引入了一个TestCoroutineContext

Pro: it makes truly testing coroutines with delay possible.优点:它使真正的delay测试协程成为可能。 You can set the CoroutineContext's virtual clock to a moment in time and verify the expected behavior.您可以将 CoroutineContext 的虚拟时钟设置为某个时刻并验证预期的行为。

Con: if your coroutine code doesn't use delay , and you just want it to execute synchronously on the calling thread, it is slightly more cumbersome to use than the TestUiContext from @bj0's answer (you need to call triggerActions() on the TestCoroutineContext to get the coroutine to execute).缺点:如果您的协程代码不使用delay ,而您只是希望它在调用线程上同步执行,那么使用它比 @bj0 的答案中的TestUiContext稍微麻烦一些(您需要在triggerActions()上调用triggerActions()使协程执行)。

Sidenote: The TestCoroutineContext now lives in the kotlinx-coroutines-test module starting with coroutines version 1.2.1, and will be marked deprecated or not exist in the standard coroutine library in versions above this version.旁注: TestCoroutineContext现在位于kotlinx-coroutines-test模块中,从协程版本 1.2.1 开始,在此版本以上的版本中,将在标准协程库中标记为已弃用或不存在。

Use TestCoroutineDispatcher, TestCoroutineScope, or Delay使用 TestCoroutineDispatcher、TestCoroutineScope 或 Delay

TestCoroutineDispatcher, TestCoroutineScope, or Delay can be used to handle a delay in a Kotlin coroutine made in the production code tested. TestCoroutineDispatcher、TestCoroutineScope 或 Delay 可用于处理在测试的生产代码中产生的 Kotlin 协程中的delay

Implement实施

In this case SomeViewModel 's view state is being tested.在这种情况下, SomeViewModel的视图状态正在被测试。 In the ERROR state a view state is emitted with the error value being true.ERROR状态下,会发出一个视图状态,错误值为 true。 After the defined Snackbar time length has passed using a delay a new view state is emitted with the error value set to false.在使用delay超过定义的 Snackbar 时间长度后,会发出一个新的视图状态,错误值设置为 false。

SomeViewModel.kt 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)
}

There are numerous ways to handle the delay .有多种方法可以处理delay advanceUntilIdle is good because it doesn't require specifying a hardcoded length. advanceUntilIdle很好,因为它不需要指定硬编码长度。 Also, if injecting the TestCoroutineDispatcher, as outlined by Craig Russell , this will be handled by the same dispatcher used inside of the ViewModel.此外,如果注入 TestCoroutineDispatcher,如Craig Russell 所述,这将由 ViewModel 内部使用的相同调度程序处理。

SomeTest.kt 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()

These will also work:这些也将起作用:

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

Error without handling the delay没有处理延迟的错误

kotlinx.coroutines.test.UncompletedCoroutinesError: Unfinished coroutines during teardown. kotlinx.coroutines.test.UncompletedCoroutinesError:拆卸期间未完成的协程。 Ensure all coroutines are completed or cancelled by your test.确保您的测试完成或取消了所有协程。

at kotlinx.coroutines.test.TestCoroutineDispatcher.cleanupTestCoroutines(TestCoroutineDispatcher.kt:178) at app.topcafes.FeedTest.cleanUpTest(FeedTest.kt:127) at app.topcafes.FeedTest.access$cleanUpTest(FeedTest.kt:28) at app.topcafes.FeedTest$topCafesTest$1.invokeSuspend(FeedTest.kt:106) at app.topcafes.FeedTest$topCafesTest$1.invoke(FeedTest.kt) at kotlinx.coroutines.test.TestBuildersKt$runBlockingTest$deferred$1.invokeSuspend(TestBuilders.kt:50) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56) at kotlinx.coroutines.test.TestCoroutineDispatcher.dispatch(TestCoroutineDispatcher.kt:50) at kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:288) at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26) at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109) at kotlinx.coroutines.AbstractCoroutine.star在 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.kt. :109) 在 kotlinx.coroutines.AbstractCoroutine.star t(AbstractCoroutine.kt:158) at 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.common.kt:84) at kotlinx.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) at 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) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.r t(AbstractCoroutine.kt:158) at kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:91) at kotlinx.coroutines.BuildersKt.async(Unknown Source) at kotlinx.coroutines.BuildersKt__.Builders_syncmon$Kt.Builders_syncmonKt.async(Builders.common.kt:91) common.kt:84) at kotlinx.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) at 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) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org .junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) 在 org.junit.r unners.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) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) at 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.startRunnerWithA unners.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) 在 org.junit.runners.ParentRunner$3.run(ParentRunner.java:57) 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) 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) 在 com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) 在 com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithA rgs(IdeaTestRunner.java:33) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58) rgs(IdeaTestRunner.java:33) 在 com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230) 在 com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM