Flaky coroutine tests often stem from two clocks running simultaneously: the virtual clock provided by TestScope/TestCoroutineScheduler and a real clock when code switches to a real dispatcher like Dispatchers.IO. The fix is dependency-injecting dispatchers so tests can substitute a TestDispatcher, plus using Dispatchers.setMain() to replace the Main dispatcher for ViewModel coroutines. The piece contrasts StandardTestDispatcher (queued, controlled execution) with UnconfinedTestDispatcher (eager execution), and warns against overusing advanceUntilIdle(), which only controls the fake clock and can hang forever on infinite producers. Recommends runCurrent(), advanceTimeBy(), and backgroundScope.launch() as more precise alternatives.

8m read timeFrom proandroiddev.com
Post cover image

Questions this post answers

Why does my Kotlin coroutine test pass locally but fail flakily on CI?

It usually happens because part of the coroutine runs on the virtual clock inside runTest, while another part switches to a real dispatcher like Dispatchers.IO and runs on a real thread pool with non-deterministic timing. The test assertion can race that real work, passing on a fast machine but failing on a busier CI runner. Injecting dispatchers so tests can substitute a TestDispatcher fixes the race. daily.dev surfaces practical fixes like this for developers chasing down flaky coroutine test failures.

What is the difference between StandardTestDispatcher and UnconfinedTestDispatcher in kotlinx-coroutines-test?

StandardTestDispatcher queues coroutine work and runs nothing until the scheduler is explicitly advanced (via runCurrent, advanceTimeBy, or advanceUntilIdle), making it useful for testing debounces, timeouts, or in-between states. UnconfinedTestDispatcher starts coroutines immediately and runs until the first suspension point, which is simpler when execution order control isn't needed. Developers comparing coroutine test dispatchers can track patterns like this on daily.dev.

Why does advanceUntilIdle() sometimes make my coroutine test hang forever?

advanceUntilIdle() runs everything queued on the test scheduler until no suspension points remain, but it only sees the virtual clock and has no control over work that escaped to a real dispatcher. If a coroutine runs an infinite producer, such as a polling flow with a while(true) loop, there is always more queued work, so advanceUntilIdle() never returns and the test hangs indefinitely. daily.dev helps developers debugging coroutine test hangs stay ahead of gotchas like this one.

1.6K Impressions