skip to content

Testing StateFlow / SharedFlow

Hot flows need a collector running in a background scope, and conflation means you can legitimately miss intermediate values. Interviewers use this to check you understand why a StateFlow test that reads .value after every update can pass while the behavior is wrong.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

When testing a StateFlow in a runTest block, how can you assert its current value without collecting it, and why does that work for StateFlow but not a plain Flow?

level: juniorimportance: must knowfreq 60%

answer

  1. StateFlow has .value, plain Flow does not
  2. Hot + conflated state holder
  3. No collector needed for current value
  4. SharedFlow uses replayCache, not .value
  5. runTest gives virtual time but .value read is synchronous

basics

~10 s

Read stateFlow.value. StateFlow always holds a current value you can read directly, so you just assert on it. A plain Flow has no stored value, so there is nothing to read.

solid answer

~40 s

StateFlow extends Flow but also exposes a synchronous .value property holding its latest state. In a runTest block you can drive the system under test (call the function that updates the StateFlow), then assert assertEquals(expected, stateFlow.value) with no collector at all. This works because StateFlow is a state holder: it always has exactly one current value and conflates updates to the newest one. A cold Flow (and a base SharedFlow without replay) keeps no stored value, so .value does not exist there — you must collect emissions to observe them. For StateFlow, reading .value is the simplest, most deterministic assertion and avoids needing virtual time or a collection coroutine when you only care about the final/current state.

code

kotlin · 6 lines
kotlin
@Test
fun toggles() = runTest {
    val flow = MutableStateFlow(false)
    flow.value = true
    assertTrue(flow.value)  // direct read, no collection
}

go deeper

for a junior

Knows StateFlow.value exists and can assert on it directly in a test.

for a middle

Explains why .value works (hot conflated state holder) and that plain Flow/SharedFlow lack it.

for a senior

Distinguishes when a direct .value read suffices vs needing advanceUntilIdle, and notes SharedFlow.replayCache.

for a principal

Frames .value reads as the deterministic default for final-state assertions and reserves collection for intermediate-sequence verification across a test suite.

## What StateFlow is `StateFlow<T>` is a **hot**, **conflated** flow that always holds exactly one value. "Hot" means it exists and holds state independently of collectors (unlike a cold `Flow`, which runs its producer fresh per collector). It exposes a synchronous read-only property: ```kotlin public val value: T ``` `MutableStateFlow<T>` adds a settable `value` (and `compareAndSet`). Because the value is always present, you can read it in a test **without any collector and without advancing virtual time**. ## Asserting in a test ```kotlin @Test fun increments() = runTest { val vm = CounterViewModel() // exposes val state: StateFlow<Int> vm.increment() // mutates the underlying MutableStateFlow assertEquals(1, vm.state.value) // direct, synchronous read } ``` `runTest` provides a `TestScope` with a virtual-time scheduler, but for a pure `.value` read you do not even need to call `advanceUntilIdle()` unless the update happens inside a launched coroutine that must run first. ## Why a plain Flow can't do this A cold `Flow<T>` has no stored state — it is a recipe that emits when collected. There is no `.value`. To observe a `Flow`, you must collect it (e.g. `.toList()` or a launched `collect`). Likewise a base `SharedFlow` with `replay = 0` keeps no value to read. ## StateFlow vs SharedFlow for this - `StateFlow` → has `.value` (replay = 1 conceptually, plus initial value required). Read it directly. - `SharedFlow` → no `.value`. Even with `replay = 1` you read the buffered value via `replayCache.lastOrNull()`, not `.value`. ## Key takeaway For StateFlow, when the assertion is about the **current/final** state, prefer reading `.value` — it is deterministic and needs no collection machinery. Use a collector only when you must verify the **sequence** of intermediate emissions.

  • What if the StateFlow is updated inside a coroutine launched by the function under test?
    Then call advanceUntilIdle() (or runCurrent()) so the launched coroutine runs on the virtual scheduler before you read .value.
  • How do you read the latest value of a SharedFlow with replay=1?
    Use sharedFlow.replayCache.lastOrNull(); SharedFlow has no .value property.

StateFlow is like a thermostat display showing the current temperature; a plain Flow is a stream of readings with no display to glance at.

saying these in an interview costs you the question

  • Claiming a plain Flow also has a .value property
  • Spinning up a collector just to read the current state
  • Confusing StateFlow.value with SharedFlow.replayCache
  • Asserting .value before advancing time when the update is in a launched coroutine

context

open as a page

Why should you launch the collection of a hot StateFlow/SharedFlow in runTest's backgroundScope rather than the test scope itself?

level: middleimportance: must knowfreq 70%

basics

~10 s

A hot flow never completes, so collecting it in the main test coroutine would hang forever. backgroundScope runs the collector alongside the test and is automatically cancelled when the test body finishes.

open as a page

A test collects a StateFlow into a list and expects three intermediate values, but only sees the first and last. Why, and how do you make the intermediate updates observable?

level: seniorimportance: should knowfreq 55%

basics

~10 s

StateFlow conflates: if several updates happen before the collector runs under virtual time, it only delivers the newest. Let the collector run between updates by advancing time, or assert only on the final value.

open as a page

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?

level: seniorimportance: should knowfreq 45%

basics

~10 s

Start 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.

open as a page

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?

level: principalimportance: nice to knowfreq 30%

basics

~10 s

WhileSubscribed 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.

open as a page