skip to content

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