Unit testing a Kotlin coroutine with delay
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()
)
@Testfun `test with delay`() { runBlocking(TestUiContext()) { launch { println("launched") } println("start") delay(5000) println("stop") }}
Runs without delay and prints:
startlaunchedstop
EDIT:
You can control where the continuation is run by customizing the dispatch
function.
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
.
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)