question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

AdvanceTimeBy has no effect within test block when using runBlockingTest

See original GitHub issue

Pretty simple repro with the following test:

    @Test
    fun test() = runBlockingTest {
        val flow = flow {
            delay(100L)
            emit("a")
        }

        flow.test {
            advanceTimeBy(101L)
            assertThat(expectItem()).isEqualTo("a")
        }
    }

However, the test above fails with the following error:

kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms
	at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:186)
	at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:156)
	at kotlinx.coroutines.test.TimedRunnable.run(TestCoroutineDispatcher.kt)
	at kotlinx.coroutines.test.TestCoroutineDispatcher.doActionsUntil(TestCoroutineDispatcher.kt:103)
	at kotlinx.coroutines.test.TestCoroutineDispatcher.advanceUntilTime(TestCoroutineDispatcher.kt:123)
	at kotlinx.coroutines.test.TestCoroutineDispatcher.advanceUntilIdle(TestCoroutineDispatcher.kt:133)
	at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:52)
	at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest$default(TestBuilders.kt:45)
	at com.repro.ReproTest.test(ReproTest.kt:16)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	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)

I suspect this is probably some nuance with TestCoroutineDispatcher plus the fact that test { … } is internally using Dispatchers.Unconfined.

Kotlin: 1.4.31 Coroutines: 1.4.3 Turbine: 0.4.1

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:10
  • Comments:7 (3 by maintainers)

github_iconTop GitHub Comments

7reactions
benkaycommented, Jun 25, 2021

@ebenfield’s example is also broken - it both makes the test take the real-world time of the delay inside the flow { }, and also the test will pass even when removing the advanceTimeBy call

i.e. this test takes 10 seconds, and also passes

  @Test
  fun brokenTest() {
    val dispatcher = TestCoroutineDispatcher()
    runBlocking(dispatcher) {
      flow { delay(10_000L); emit(Unit) }
        .test {
          dispatcher.advanceTimeBy(5_000L)
          expectItem()
          expectComplete()
        }
    }
  }

This is because turbine ignores the current dispatcher when launching the flow, so delay() actually delays, and the test { } block does run in the test dispatcher, which means expectItem’s timeouts don’t work

What does work is to use flowOn

  @Test
  fun workingTest() {
    runBlocking {
	  val dispatcher = TestCoroutineDispatcher()
      flow {
        delay(10_000L); emit(Unit)
      }
        .flowOn(dispatcher)
        .test {
          dispatcher.advanceTimeBy(11_000L)
          expectItem()
          expectComplete()
        }
    }
  }

Now the body of the flow is the thing running in the test dispatcher, so the delay controller works, and also so do the turbine timeouts.

IMO it would be nice if the runBlockingTest example was made to work somehow as it feels surprising that it doesn’t. Also the brokenTest example is dangerous as with short delays it’s not clear that anything’s wrong, and it’s silently just not testing anything. Perhaps turbine should internally force the expectEvent() timeout to be 0 when using the test dispatcher?

4reactions
PaulWoitaschekcommented, Dec 23, 2021

With the help of @dkhalanskyjb I found a way to make it work with the new runTest:

  suspend fun <T> Flow<T>.test2(
    timeout: Duration = 1.seconds,
    validate: suspend FlowTurbine<T>.() -> Unit,
  ) {
    val testScheduler = coroutineContext[TestCoroutineScheduler]
    return if (testScheduler == null) {
      test(timeout, validate)
    } else {
      flowOn(UnconfinedTestDispatcher(testScheduler))
        .test(timeout, validate)
    }
  }

Does it make sense to incoroprate this behavior direclty into turbine?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Kotlin - Test inner Coroutines with delay using runBlockingTest
I know I can test code using delay() with runBlockingTest and advanceTimeBy() but the thing is loadingJob.cancel() will be called before I ...
Read more >
kotlinx-coroutines-test
This is helpful when one wants to execute a test in situations where the platform Main dispatcher is not available, or to replace...
Read more >
Testing Kotlin Coroutines - Kt. Academy
We just use runBlocking , and there is nearly no difference between testing how suspending and blocking functions behave. Testing time dependencies. The ......
Read more >
Testing Coroutines — Update 1.6.0 | by Ralf Stuckert - Medium
The building blocks of the kotlinx-coroutines-test module have changed a bit. ... You may now use multiple TestDispatcher s in a test, but...
Read more >
Unit Testing Kotlin Channels & Flows - Android Summit
In this talk, I will share with you how to implement and test ... how to use features in the coroutines library such...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found