skip to content

Flow Testing

Asserting on a stream of emissions is harder than asserting on a value, which is why Flow testing has its own tools and its own pitfalls with hot flows. Expect this immediately after any Flow question.

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

explore

questions

15

How do you collect all emissions from a finite Flow in a test and assert the exact sequence of values?

level: juniorimportance: must knowfreq 78%

answer

  1. runTest gives a coroutine scope for suspend calls
  2. toList() = terminal, suspends to completion, returns ordered List
  3. assertEquals(expected, actual) — expected first
  4. List equality is order-sensitive
  5. Only for finite/completing flows

basics

~10 s

Call toList() on the flow inside a runTest block to gather every emitted value into a List, then compare it to the expected list with assertEquals(expected, actual).

solid answer

~40 s

Wrap the test in runTest { } so suspend functions like Flow.toList() can be called. toList() is a terminal operator that suspends until the flow completes, collecting every emission into a List in emission order. Assert with assertEquals(expected, actual) where expected is the literal list you anticipate, e.g. assertEquals(listOf(1, 2, 3), flowOf(1, 2, 3).toList()). Because List equality is order-sensitive and element-wise, this checks both the values and their order. Only use toList() for finite flows that complete on their own; an infinite flow (e.g. a hot StateFlow) would hang the test. Keep the expected value first in assertEquals so failure messages read 'expected ... but was ...' correctly.

code

kotlin · 7 lines
kotlin
@Test
fun mapsValues() = runTest {
    val result = flowOf(1, 2, 3)
        .map { it * 10 }
        .toList()
    assertEquals(listOf(10, 20, 30), result)
}

go deeper

for a junior

Knows to wrap in runTest, call toList(), and assertEquals against a literal list.

for a middle

Explains that toList() is a terminal suspend operator that preserves order and only suits finite flows.

for a senior

Discusses runTest's virtual time, why infinite flows hang, and expected-first convention for readable diffs.

for a principal

Frames choice of collection strategy (toList vs take vs first vs Turbine) as a testability concern and guides team conventions.

## What we are testing A `Flow<T>` in Kotlin coroutines is a cold asynchronous stream: nothing runs until a **terminal operator** starts collection. To test a flow we need to (1) run it to completion, (2) capture what it emitted, and (3) compare that against what we expected. ## `runTest` — the test coroutine builder `Flow.toList()` is a `suspend` function (it can pause and resume), so it can only be called from a coroutine. `runTest { }` (from `kotlinx-coroutines-test`) creates a coroutine scope for tests. Inside it you can call suspend functions directly. It also uses virtual/skipped time, so delays don't slow the test down. ```kotlin import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals class NumbersTest { @Test fun emitsThreeNumbers() = runTest { val flow = flowOf(1, 2, 3) val result = flow.toList() // terminal operator, suspends until done assertEquals(listOf(1, 2, 3), result) // expected first, actual second } } ``` ## `toList()` — the terminal collector - It is a **terminal operator**: it triggers collection and consumes the whole stream. - It **suspends** until the flow naturally completes, then returns a `List<T>` in emission order. - It is the standard way to snapshot a finite flow's full output. - You can pass an existing mutable list: `flow.toList(destination)`. ## Asserting with `assertEquals` `assertEquals(expected, actual)` from `kotlin.test` uses `==` (structural equality). For `List`, equality is **ordered and element-wise**: `listOf(1, 2)` equals `listOf(1, 2)` but not `listOf(2, 1)`. So a single `assertEquals` simultaneously checks the values AND their order. Convention: put the **expected** value first so the failure message reads `expected:<...> but was:<...>`. ## Finite vs infinite `toList()` only works on flows that **complete**. A flow built from a never-ending source (e.g. `MutableStateFlow`, an infinite `flow { while(true) ... }`) will never finish, so `toList()` would suspend forever and the test would hang/time out. For those you take a bounded prefix or use other tools. ## Recap of keywords `suspend`, `Flow`, terminal operator, `runTest`, `toList()`, `assertEquals`, structural equality (`==`).

  • What happens if you call toList() on an infinite flow inside runTest?
    It never returns because the flow never completes; runTest will hang and eventually fail with a timeout. Use take(n).toList() or first() to bound it.
  • Does assertEquals on two lists check ordering?
    Yes. List.equals is order-sensitive and element-by-element, so the assertion fails if values differ OR if the order differs.

toList() is like letting a vending machine dispense every item it will give, then counting them in the order they dropped.

saying these in an interview costs you the question

  • Calling toList() outside any coroutine/runTest and expecting it to compile
  • Claiming toList() ignores emission order
  • Using toList() on a StateFlow or infinite flow
  • Putting actual before expected and misreading failure messages
  • Thinking toList() returns before the flow completes

context

open as a page

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%

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.

open as a page

What is Turbine and why use flow.test { } instead of toList() when testing a Kotlin Flow?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Turbine is a small testing library for Kotlin Flows. Its flow.test { } block lets you wait for and check each emitted value one at a time, instead of collecting everything into a list first.

open as a page

What is the difference between flow.first(), flow.take(n).toList(), and flow.toList() when collecting in a test, and when do you use each?

level: middleimportance: must knowfreq 70%

basics

~20 s

first() grabs only the very first emission and stops. take(n).toList() collects the first n emissions then stops. toList() collects everything until the flow finishes. Use the bounded ones when the flow may not complete on its own.

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

Explain awaitItem(), awaitComplete(), and awaitError() in Turbine. What does each assert and return?

level: middleimportance: must knowfreq 65%

basics

~10 s

awaitItem() waits for the next value and returns it. awaitComplete() checks the flow finished normally. awaitError() checks the flow stopped because of an exception and returns that exception.

open as a page

What does cancelAndIgnoreRemainingEvents() do, and why is it usually required when testing a StateFlow or other hot flow with Turbine?

level: seniorimportance: must knowfreq 55%

basics

~20 s

It stops Turbine from collecting the flow and throws away any values still waiting. Hot flows like StateFlow never end on their own, so you call it to finish the test cleanly without failing on leftover events.

open as a page

A flow uses delay() between emissions. How does runTest let you collect it with toList() without the test actually waiting in real time?

level: middleimportance: should knowfreq 58%

basics

~10 s

runTest uses a virtual clock. Delays inside the flow are skipped instantly instead of really pausing, so toList() still collects all the values but the test finishes in milliseconds.

open as a page

When and how do you use expectNoEvents() in Turbine, and what's a common pitfall with it?

level: middleimportance: should knowfreq 40%

basics

~10 s

expectNoEvents() checks that, right now, the flow has not produced any new value, completion, or error. You use it to prove something did NOT happen yet, like after a filtered input.

open as a page

How do you assert that a flow emits some values and then throws, or that it emits nothing at all, when collecting with toList()?

level: seniorimportance: should knowfreq 47%

basics

~20 s

If a flow throws, toList() rethrows that exception, so wrap it in assertFailsWith. To capture values emitted before the failure, collect into a list manually with collect{}. For an empty flow, toList() returns an empty list you assert with assertEquals(emptyList(), result).

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

How does Turbine's await timeout interact with kotlinx-coroutines-test virtual time, and how do you tune it?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Each await waits up to a timeout (3 seconds by default). Inside runTest, the flow's delays are skipped using virtual time, but a flow that truly never emits still hits the timeout and fails the test.

open as a page

You wrote flow.toList() and asserted an exact ordered list, but the test is flaky—sometimes the order differs. What likely causes this and how do you make collection deterministic?

level: seniorimportance: nice to knowfreq 33%

basics

~20 s

A single sequential flow emits in a fixed order, so flakiness usually means concurrency: the flow uses flatMapMerge, merge, channels, or a real dispatcher that interleaves results. Use ordering-preserving operators or sort the result before asserting.

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