43

I'm trying to unit test a Kotlin coroutine that uses delay(). For the unit test I don't care about the delay(), it's just slowing the test down. I'd like to run the test in some way that doesn't actually delay when delay() is called.

I tried running the coroutine using a custom context which delegates to 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.

AdamHurwitz
  • 9,758
  • 10
  • 72
  • 134
Erik Browne
  • 1,393
  • 1
  • 14
  • 19

4 Answers4

29

If you don't want any delay, why don't you simply resume the continuation in the schedule call?:

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. Note that this still suspends at delay, so other coroutines can still run (like 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.

bj0
  • 7,893
  • 5
  • 38
  • 49
  • And `dispatch()` can just be `block.run()`, which keeps execution in the main thread. – Erik Browne Mar 03 '18 at 18:04
  • True, and that might make sense for testing, though he seemed to want it to use CommonPool. – bj0 Mar 04 '18 at 07:47
  • I'm the OP, and I'd rather it use the same thread. Make the change and I'll accept your answer. – Erik Browne Mar 05 '18 at 17:54
  • 2
    The `Delay` interface is now an internal API in the KotlinX coroutine library. This class, and any class which uses it, must be marked with the `@InternalCoroutinesApi` annotation. – Erik Browne Feb 19 '19 at 18:04
27

In kotlinx.coroutines v1.6.0 the kotlinx-coroutines-test module was updated. It allows tests to use the runTest() method and TestScope to test suspending code, automatically skipping delays.

See the documentation for details on how to use the module.

Previous Answer

In kotlinx.coroutines v1.2.1 they added the kotlinx-coroutines-test module. It includes the runBlockingTest coroutine builder, as well as a TestCoroutineScope and TestCoroutineDispatcher. They allow auto-advancing time, as well as explicitly controlling time for testing coroutines with delay.

Erik Browne
  • 1,393
  • 1
  • 14
  • 19
  • 1
    Great resources shared @Erik Browne! Specifically, how would you implement `runBlockingTest`, `TestCoroutineScope`, or `TestCoroutineDispatcher` to handle the `delay` in the unit test? – AdamHurwitz Jun 15 '20 at 19:39
  • I've expanded upon this solution with a specific implementation [here](https://stackoverflow.com/a/62397243/2253682). – AdamHurwitz Jun 15 '20 at 21:22
  • 1
    In kotlinx.coroutines v1.6.0 they revamped the testing library, replacing `TestCoroutineScope` and `TestCoroutineDispatcher` with `TestScope` and `TestDispatcher`. – Erik Browne Nov 22 '21 at 19:47
  • 1
    @ErikBrowne I recommend that you edit your answer to highlight the recent recommendations with the new version of kotlin coroutines (1.6.0). – Amokrane Chentir Feb 01 '22 at 18:38
15

Use TestCoroutineDispatcher, TestCoroutineScope, or Delay

TestCoroutineDispatcher, TestCoroutineScope, or Delay can be used to handle a delay in a Kotlin coroutine made in the production code tested.

Implement

In this case SomeViewModel's view state is being tested. In the ERROR state a view state is emitted with the error value being 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.

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. advanceUntilIdle is good because it doesn't require specifying a hardcoded length. Also, if injecting the TestCoroutineDispatcher, as outlined by Craig Russell, this will be handled by the same dispatcher used inside of the 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()

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. 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.start(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.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) 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.startRunnerWithArgs(IdeaTestRunner.java:33) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

AdamHurwitz
  • 9,758
  • 10
  • 72
  • 134
  • 1
    if I use the `testDispatcher.advanceTimeBy` I get that unfinished coroutines error and I'm not sure how to resolve that. `testDispatcher.advanceUntilIdle` works nicely though – hmac Jun 17 '20 at 14:17
  • That is good to hear `advanceUntilIdle` does the job for you. Running the test in a `TestCoroutineDispatcher().runBlockingTest{...}` may resolve the unfinished coroutine error as [outlined by Craig Russell here](https://craigrussell.io/2019/11/unit-testing-coroutine-suspend-functions-using-testcoroutinedispatcher/). – AdamHurwitz Jun 17 '20 at 15:28
5

In kotlinx.coroutines v0.23.0 they introduced a TestCoroutineContext.

Pro: it makes truly testing coroutines with delay possible. You can set the CoroutineContext's virtual clock to a moment in time and verify the expected behavior.

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).

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.

Bwvolleyball
  • 2,593
  • 2
  • 19
  • 31
Erik Browne
  • 1,393
  • 1
  • 14
  • 19
  • A quick update that the `TestCoroutineContext` has already been deprecated in later versions of the coroutines library. – Bwvolleyball Jul 24 '19 at 20:08
  • In v1.2.1 they added the experimental [kotlinx-coroutines-test](https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-test) module, including `runBlockingTest`, `TestCoroutineScope`, and `TestCoroutineDispatcher`. – Erik Browne Jul 24 '19 at 22:17