skip to content

Turbine for Flow

Turbine gives you a test scope where you await items, completion, or errors one at a time and assert nothing else arrived. It is the standard answer for testing a flow that does not simply terminate on its own.

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

questions

5

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

level: juniorimportance: must knowfreq 70%

answer

  1. Cash App library: app.cash.turbine
  2. flow.test { } gives ReceiveTurbine receiver
  3. awaitItem / awaitComplete / awaitError
  4. Works for infinite/hot flows; toList does not
  5. Run inside runTest for virtual time

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.

solid answer

~40 s

Turbine (from Cash App) is a coroutine-friendly testing library for Kotlin Flow. The extension flow.test { } runs the flow inside a test scope and gives you a suspending API to assert emissions in order: awaitItem() pulls the next value, awaitComplete() asserts the flow finished, awaitError() asserts it threw. Compared to flow.toList(), which only works for finite flows and hides timing, Turbine works with infinite/hot flows, asserts the exact sequence and terminal event, fails if you forget unconsumed events, and integrates with runTest's virtual time. You typically wrap it in runTest { } so delays are skipped. It makes order, completion, and error explicit rather than implicit.

code

kotlin · 8 lines
kotlin
@Test
fun example() = runTest {
    flowOf("a", "b").test {
        assertEquals("a", awaitItem())
        assertEquals("b", awaitItem())
        awaitComplete()
    }
}

go deeper

for a junior

Knows Turbine tests Flows and that flow.test { } awaits items one at a time instead of toList().

for a middle

Explains why toList() fails on hot/infinite flows and that Turbine asserts ordering plus terminal events.

for a senior

Connects Turbine to runTest virtual time and the unconsumed-events failure as an intentional correctness guard.

for a principal

Frames Turbine choice in a team testing strategy: when to assert sequences explicitly vs. collect, and how it shapes deterministic flow tests.

## What Turbine is **Turbine** is a small testing library (by Cash App, `app.cash.turbine:turbine`) for asserting on **Kotlin `Flow`** emissions. A `Flow` is a cold (or hot) asynchronous stream of values produced over time; testing it means checking *which* values come out, *in what order*, and *how the stream ends* (normally or with an error). ## The core entry point: `flow.test { }` `Flow<T>.test { }` is a `suspend` extension that **collects** the flow in the background and exposes a `ReceiveTurbine<T>` receiver. Inside the block you call suspending await functions that consume events one by one: - `awaitItem(): T` — suspends until the next emitted value, returns it (or fails on completion/error/timeout). - `awaitComplete()` — asserts the flow completed normally. - `awaitError(): Throwable` — asserts the flow terminated with an exception and returns it. - `expectNoEvents()` — asserts nothing has been emitted yet (no item, completion, or error pending). - `cancelAndIgnoreRemainingEvents()` — cancels collection and drops anything still buffered. ## Why not just `toList()`? `flow.toList()` collects **everything** into a `List`, then you assert on the list. Problems: - It **never returns for infinite/hot flows** (e.g. `StateFlow`, `MutableSharedFlow`) — they don't complete. - It **hides timing and ordering semantics** behind one bulk assertion. - It doesn't distinguish *normal completion* from *error* cleanly. - It can mask **unconsumed events**; Turbine, by contrast, **fails the test if events are left unhandled** at the end of the block, catching accidental extra emissions. ## Typical usage with `runTest` Turbine requires a coroutine context. You run it inside `kotlinx.coroutines.test.runTest { }`, which provides a `TestScope` with **virtual time** so `delay()` is skipped automatically. ```kotlin @Test fun emitsThenCompletes() = runTest { flowOf(1, 2, 3).test { assertEquals(1, awaitItem()) assertEquals(2, awaitItem()) assertEquals(3, awaitItem()) awaitComplete() } } ``` Here the cold `flowOf` emits 1, 2, 3 and then **completes**, so we await three items and one completion. If we forgot `awaitComplete()` (or an item), Turbine would report unconsumed events and fail. ## Key takeaways - Turbine = ordered, explicit, terminal-aware Flow assertions. - `awaitItem`/`awaitComplete`/`awaitError` make the contract visible. - Unconsumed events are a **test failure**, which is a feature.

  • Why does Turbine fail if you don't consume every event?
    It treats leftover items/terminal events as a sign your test missed something; forcing you to assert each one prevents silently passing tests that ignore extra emissions.
  • Does flow.test { } work on a StateFlow?
    Yes — that's a main reason to use it. StateFlow never completes, so toList() would hang, but test { } lets you await the current value and then cancel.

toList() is reading a whole transcript after a call ends; flow.test { } is listening live and confirming each word as it's spoken.

saying these in an interview costs you the question

  • Claiming Turbine replaces runTest (it runs inside it)
  • Saying toList() works fine for StateFlow/SharedFlow
  • Not knowing flow.test exposes await* functions
  • Thinking unconsumed events are silently ignored

context

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

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