How do you write a deterministic runTest for a MutableSharedFlow (replay = 0) used as a one-shot event channel, given it has no .value and emissions sent before subscription are lost?
answer
- SharedFlow replay=0 has no .value and no buffer
- Emit with zero subscribers is dropped
- Subscribe eagerly before emitting
- UnconfinedTestDispatcher starts collector immediately
- subscriptionCount / replayCache for ordering or replay
basics
~10 sStart the collector in backgroundScope first (eagerly, so it subscribes), then emit the event. SharedFlow with replay 0 has no stored value and drops events emitted before any subscriber exists.
solid answer
~40 sA MutableSharedFlow(replay = 0) is hot with no buffer and no .value, so an emit() with zero subscribers is dropped. The test must guarantee the collector is subscribed before the event fires. Launch the collector in backgroundScope using UnconfinedTestDispatcher(testScheduler) so it subscribes eagerly when launched (StandardTestDispatcher would defer it until you advance, risking a lost event). Then trigger the emit, advanceUntilIdle(), and assert on the captured events. If you cannot guarantee subscription ordering, configure replay or extraBufferCapacity so early emissions are buffered. For one-shot UI events this is the canonical shape: subscribe, act, advance, assert. Reading replayCache only helps when replay > 0.
code
kotlin · 13 lines@Test
fun waitsForSubscriberThenEmits() = runTest {
val events = MutableSharedFlow<String>()
val received = mutableListOf<String>()
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
events.collect { received.add(it) }
}
// ensure subscription before emitting
events.subscriptionCount.first { it > 0 }
events.emit("go")
advanceUntilIdle()
assertEquals(listOf("go"), received)
}go deeper
Knows SharedFlow with replay 0 has no stored value and you must collect it.
Understands that emissions before subscription are dropped and the collector must start first.
Uses UnconfinedTestDispatcher and/or subscriptionCount to guarantee subscription ordering deterministically.
Chooses replay/buffer configuration as an API design decision so the event contract is testable and race-free across the team.
## SharedFlow basics relevant to testing `SharedFlow<T>` is a hot flow configurable via `MutableSharedFlow(replay, extraBufferCapacity, onBufferOverflow)`: - `replay` — number of recent values re-emitted to **new** subscribers. - `extraBufferCapacity` — buffer slots beyond replay for fast producers. - With `replay = 0` and no buffer, an `emit()` made while there are **zero subscribers** is simply **dropped** — there is no `.value` and nothing is stored. This is the classic shape for **one-shot events** (navigation, snackbars) where you don't want re-delivery on re-subscription. ## The hazard in tests If you emit before the collector subscribes, the event vanishes and the test fails non-obviously. With the default **`StandardTestDispatcher`**, a `backgroundScope.launch { flow.collect { } }` does **not** actually start collecting until the scheduler advances — so naive ordering can emit into the void. ## Deterministic pattern ```kotlin @Test fun emitsNavigationEvent() = runTest { val events = MutableSharedFlow<Nav>() // replay = 0 val received = mutableListOf<Nav>() // 1. Subscribe eagerly BEFORE emitting backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { events.collect { received.add(it) } } // 2. Now emit events.emit(Nav.Home) advanceUntilIdle() // 3. Assert assertEquals(listOf(Nav.Home), received) } ``` `UnconfinedTestDispatcher(testScheduler)` makes the launched coroutine **start eagerly** (run up to its first suspension immediately), so `collect` is subscribed before `emit`. It still shares `testScheduler`, keeping virtual time unified. ## Confirming subscription explicitly For extra robustness you can await a subscriber before emitting: ```kotlin val events = MutableSharedFlow<Nav>() backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { events.collect { received += it } } // wait until the collector is actually subscribed yield() events.emit(Nav.Home) ``` Or observe `events.subscriptionCount.first { it > 0 }` to block until at least one subscriber. ## When you can't control ordering: buffer it If the system under test emits during construction (before your test can subscribe), give the flow a buffer or replay: ```kotlin MutableSharedFlow<Nav>(replay = 1) // new subscriber gets last event // or MutableSharedFlow<Nav>(extraBufferCapacity = 8) // early emits buffered ``` Then read `events.replayCache` (only meaningful when `replay > 0`) — SharedFlow still has no `.value`. ## Key takeaway SharedFlow(replay=0) drops emissions made before subscription. In tests: subscribe first via `backgroundScope` + `UnconfinedTestDispatcher`, optionally await `subscriptionCount`, then emit and advance. Use `replay`/`extraBufferCapacity` only when subscription ordering can't be guaranteed.
- Why does StandardTestDispatcher make this test fragile?Coroutines launched on it do not start until you advance the scheduler, so the collector may not be subscribed yet when emit() runs, dropping the event.
- When is reading replayCache valid for a SharedFlow?Only when replay > 0; with replay = 0 the replayCache is always empty and SharedFlow never has a .value.
A replay=0 SharedFlow is a live radio broadcast: if no one has tuned in yet, the words are gone — you must turn the radio on before the announcer speaks.
saying these in an interview costs you the question
- Expecting SharedFlow(replay=0) to have a .value like StateFlow
- Emitting before launching the collector and wondering why nothing is received
- Assuming backgroundScope.launch subscribes immediately on StandardTestDispatcher
- Using replayCache with replay=0 and expecting a value
- Adding sleeps to 'fix' the ordering instead of awaiting subscriptionCount