You test a StateFlow created with stateIn(scope, SharingStarted.WhileSubscribed(), initial). Under virtual time, .value stays at the initial value even after the upstream should have emitted. Why, and how do you make it update in the test?
answer
- WhileSubscribed: upstream runs only with subscribers
- No collector → .value stuck at initialValue
- Sharing scope must use testScheduler (backgroundScope)
- Add eager subscriber + advanceUntilIdle
- Eagerly avoids needing a subscriber; mind stopTimeoutMillis
basics
~10 sWhileSubscribed only starts the upstream when there is a subscriber. With no collector, the StateFlow stays at its initial value. Add a collector in backgroundScope and advance time, or use SharingStarted.Eagerly in the test.
solid answer
~40 sstateIn with SharingStarted.WhileSubscribed() lazily starts (and stops) the upstream based on subscription count. In a test with no active collector, the upstream never runs, so .value stays at the initial seed regardless of how much virtual time you advance. To make it update: launch a collector in backgroundScope (eagerly, e.g. UnconfinedTestDispatcher(testScheduler)) so the StateFlow becomes subscribed, then advanceUntilIdle()/advanceTimeBy() to let the upstream emit; now .value reflects upstream. The sharing scope must also use the test scheduler (pass backgroundScope or a scope built on testScheduler) so virtual time drives it. Alternatively, in tests use SharingStarted.Eagerly to start upstream immediately without needing a subscriber. Also mind WhileSubscribed's stopTimeoutMillis when advancing time around subscriber removal.
code
kotlin · 9 lines@Test
fun whileSubscribedNeedsSubscriber() = runTest {
val upstream = flow { emit("A"); delay(50); emit("B") }
val state = upstream.stateIn(backgroundScope, SharingStarted.WhileSubscribed(), "init")
assertEquals("init", state.value) // no subscriber yet
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { state.collect {} }
advanceUntilIdle()
assertEquals("B", state.value) // upstream ran once subscribed
}go deeper
Knows stateIn turns a cold flow into a StateFlow with an initial value.
Understands WhileSubscribed starts upstream only when subscribed, so a collector is needed in the test.
Ensures the sharing scope uses testScheduler and adds an eager subscriber before advancing time.
Reasons about SharingStarted semantics, stopTimeoutMillis virtual-time behavior, and whether to vary the strategy in tests vs production to keep both correct and deterministic.
## What stateIn + SharingStarted does `Flow.stateIn(scope, started, initialValue)` converts a cold upstream `Flow` into a hot `StateFlow`, sharing one upstream collection among subscribers. The `started: SharingStarted` strategy controls **when the upstream actually runs**: - `SharingStarted.Eagerly` — upstream starts immediately when `stateIn` is called and runs until the `scope` is cancelled. - `SharingStarted.Lazily` — starts on the **first** subscriber, then stays active. - `SharingStarted.WhileSubscribed(stopTimeoutMillis, replayExpirationMillis)` — starts when subscriber count goes 0→1 and **stops** the upstream `stopTimeoutMillis` after it drops to 0. ## Why .value is stuck at the initial value With `WhileSubscribed()` and **no collector**, `subscriptionCount` is 0, so the upstream **never starts**. The StateFlow only ever holds the `initialValue` you passed to `stateIn`. Advancing virtual time does nothing because there is no upstream coroutine scheduled to run. This surprises people who expect `.value` to reflect upstream just because time passed. ## Fix 1 — provide a subscriber and the test scheduler ```kotlin @Test fun stateInUpdates() = runTest { val upstream = flow { emit(1); delay(100); emit(2) } val state = upstream.stateIn( scope = backgroundScope, // uses testScheduler started = SharingStarted.WhileSubscribed(), initialValue = 0, ) // become a subscriber so WhileSubscribed starts the upstream backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { state.collect { /* keep subscription alive */ } } advanceUntilIdle() assertEquals(2, state.value) } ``` Two things matter: (a) the **sharing scope** is `backgroundScope` (or any scope on `testScheduler`) so virtual time drives the upstream; (b) there is an **active subscriber** so `WhileSubscribed` actually starts. ## Fix 2 — use Eagerly in tests If you only care about producing the final state and not about subscription semantics, swap the strategy in the test: ```kotlin val state = upstream.stateIn(backgroundScope, SharingStarted.Eagerly, 0) advanceUntilIdle() assertEquals(2, state.value) // no explicit collector needed ``` ## Watch out: stopTimeoutMillis `WhileSubscribed(stopTimeoutMillis = 5000)` keeps the upstream alive 5s (virtual) after the last subscriber leaves. When you cancel your collector and `advanceTimeBy`, crossing that timeout will stop the upstream — relevant if a test asserts behavior after subscription drops. ## Key takeaway `.value` frozen at the initial value under `WhileSubscribed()` is **correct lazy behavior**: no subscriber ⇒ no upstream. Make the StateFlow update by (1) running it on a `testScheduler`-backed scope, (2) adding an eager subscriber in `backgroundScope`, then advancing time — or use `SharingStarted.Eagerly` for the test. Account for `stopTimeoutMillis` when simulating subscriber churn.
- Why must the sharing scope use the test scheduler?So the shared upstream coroutine runs on virtual time; a scope on a real dispatcher would not respond to advanceUntilIdle and would behave non-deterministically.
- How does stopTimeoutMillis affect a test that cancels the collector mid-way?After the last subscriber leaves, the upstream keeps running for stopTimeoutMillis of virtual time; advancing past it stops the upstream, which you must account for in assertions.
WhileSubscribed is a motion-sensor light: it only turns on when someone is in the room. Standing outside (no subscriber) and waiting (advancing time) keeps it dark.
saying these in an interview costs you the question
- Expecting WhileSubscribed StateFlow to update with no subscriber
- Using a sharing scope not tied to testScheduler
- Thinking advanceUntilIdle alone starts a WhileSubscribed upstream
- Ignoring stopTimeoutMillis when simulating subscriber removal
- Switching to Eagerly in production just to make the test pass