How do you deterministically test a Kotlin Flow, including hot flows like StateFlow that never complete?
answer
- Cold completing flow: flow.toList() under runTest
- StateFlow.toList() hangs — never completes
- Turbine: test { awaitItem(); cancelAndIgnoreRemainingEvents() }
- awaitItem/awaitComplete/awaitError/expectNoEvents
- StateFlow.value after advanceUntilIdle for latest-state checks
basics
~20 sFor a normal flow you collect its values into a list inside runTest and check them. For a flow that never ends, like StateFlow, you use a tool such as Turbine to read items one at a time and then stop collecting.
solid answer
~40 sCold flows that complete can be tested by collecting inside `runTest`: `flow.toList() shouldBe listOf(...)`, since `runTest`'s virtual clock makes any internal `delay`s deterministic. Hot flows (`StateFlow`, `SharedFlow`) never complete, so `toList()` would hang; you launch collection in a separate coroutine and cancel it, or — idiomatically — use **Turbine** (`app.cash.turbine`): `flow.test { awaitItem() ; ... ; awaitComplete() / cancelAndIgnoreRemainingEvents() }`. Turbine gives `awaitItem`, `awaitError`, `awaitComplete`, and `expectNoEvents`, and fails if unconsumed events remain, catching missed emissions. For `StateFlow` you often assert on `.value` after advancing time, or collect with Turbine while driving the producer. Combine with `advanceUntilIdle()` to flush queued work before asserting. The key is always running under `runTest` so emission ordering and delays are virtual and deterministic.
go deeper
Knows you collect a flow's emissions and compare against an expected list.
Tests cold flows with toList() under runTest and knows StateFlow won't complete.
Uses Turbine (or launch+cancel) for hot flows, drives virtual time, and asserts emission sequences precisely.
Standardizes Flow-testing patterns (Turbine vs value assertions), backpressure/conflation awareness, and flake-free CI conventions.
## Cold vs Hot Flows - A **cold flow** (`flow { ... }`) runs its producer per collector and usually **completes**. - A **hot flow** (`StateFlow`, `SharedFlow`) is always active and **never completes** — collection only ends on cancellation. This distinction drives how you test them. ## Testing Cold, Completing Flows Collect into a list under `runTest`: ```kotlin @Test fun `emits 1,2,3`() = runTest { val flow = flow { emit(1); delay(100); emit(2); emit(3) } assertEquals(listOf(1, 2, 3), flow.toList()) } ``` `runTest`'s virtual `TestCoroutineScheduler` skips the `delay(100)`, so this is instant and deterministic. ## Testing Hot Flows (the trap) `stateFlow.toList()` **hangs forever** because the flow never completes. Two solutions: ### 1. Manual launch + cancel ```kotlin val results = mutableListOf<Int>() val job = launch { stateFlow.toList(results) } advanceUntilIdle() job.cancel() assertEquals(listOf(0, 1), results) ``` ### 2. Turbine (idiomatic) ```kotlin import app.cash.turbine.test @Test fun `state flow emits updates`() = runTest { viewModel.state.test { assertEquals(State.Loading, awaitItem()) viewModel.load() assertEquals(State.Loaded, awaitItem()) cancelAndIgnoreRemainingEvents() } } ``` Turbine API: `awaitItem()`, `awaitComplete()`, `awaitError()`, `expectNoEvents()`, `cancelAndIgnoreRemainingEvents()`. It **fails the test if events are left unconsumed**, catching extra or missed emissions. ## StateFlow value assertions Because `StateFlow` always holds a current value, you can also advance time then assert on `.value` directly — simpler when you only care about the latest state, not the sequence. ## Always Use runTest Wrapping in `runTest` makes the scheduler virtual so emission ordering and `delay`-driven timing are reproducible; pair with `advanceUntilIdle()` / `advanceTimeBy()` to flush producer work before reading.
- Why does calling toList() on a StateFlow in a test hang?StateFlow is a hot flow that never completes, so toList() waits for a terminal event that never arrives. You must cancel collection or use Turbine.
- What does Turbine do if your test leaves emissions unconsumed?It fails the test, ensuring you assert on every emission and catch unexpected extra events; cancelAndIgnoreRemainingEvents() opts out explicitly.
A cold flow is a song with an ending you can record fully; a hot flow is a live radio stream — you must tune in, capture a few moments, then switch off, which is what Turbine's await + cancel does.
saying these in an interview costs you the question
- Calling toList() on a StateFlow/SharedFlow and wondering why the test hangs
- Testing flows outside runTest, getting nondeterministic timing
- Forgetting to cancel manual collection coroutines
- Not advancing virtual time before asserting on emissions
- Ignoring unconsumed-event failures Turbine raises