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.
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.
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.
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
.
TestCoroutineDispatcher, TestCoroutineScope, or Delay can be used to handle a delay
in a Kotlin coroutine made in the production code tested.
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)
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With