skip to content

In Dart's package:test, how do you assert what a Stream emits, including an error event and the done event?

level: middleimportance: should knowfreq 38%

answer

  1. stream matchers, not lists
  2. emitsInOrder for a sequence
  3. emitsError for the error event
  4. emitsDone to forbid extra events
  5. StreamQueue for step-by-step checks

basics

~20 s

Use stream matchers: expect(stream, emitsInOrder([a, b, emitsDone])) checks a sequence and the end; emits(x) matches one event, emitsError(m) an error event, and emitsThrough skips ahead. Wrap a StreamQueue and await expectLater to check events step by step.

solid answer

~40 s

`package:test` re-exports matcher's **stream matchers**. `emits(x)` matches one data event; `emitsError(isA<FormatException>())` matches an error event; `emitsDone` matches the done event; `emitsInOrder([...])` chains them; `emitsThrough(x)` skips events until one matches; `mayEmit`, `emitsAnyOf` and `neverEmits` cover optional, alternative and forbidden events. By default more events may follow a successful match, so add `emitsDone` when the stream must end there. Matching is asynchronous, so `expect` returns immediately and the test waits for the result; use `await expectLater(...)` when later steps depend on it. A single-subscription `Stream` can be listened to only once, so for several checks against one stream wrap it in a `StreamQueue` from `package:async`: stream matchers accept a queue and consume just the events they match.

code

dart · 24 lines
dart
import 'dart:async';

import 'package:test/test.dart';

void main() {
  test('emits two dates, then an error, then closes', () async {
    final controller = StreamController<DateTime>();
    controller
      ..add(DateTime(2026, 1, 1))
      ..add(DateTime(2026, 1, 2))
      ..addError(const FormatException('bad line'));
    unawaited(controller.close());

    await expectLater(
      controller.stream,
      emitsInOrder([
        DateTime(2026, 1, 1),
        DateTime(2026, 1, 2),
        emitsError(isA<FormatException>()),
        emitsDone,
      ]),
    );
  });
}

go deeper

for a junior

Recall emits, emitsInOrder and emitsDone, and that the check is asynchronous.

for a middle

Explain error events with emitsError, why extra events pass without emitsDone, and how StreamQueue allows step-by-step checks.

for a senior

Show you test streams driven by method calls without collecting everything, and choose between broadcast streams and StreamQueue to match production behaviour.

for a principal

Decide how strict stream contracts should be in shared libraries, weighing exact sequences against tests that break on harmless extra events.

## Why lists are the wrong tool A common first attempt at testing a stream is `expect(await stream.toList(), [a, b])`. It works for short, finite streams, but it waits for the stream to **close**, cannot express "an error happened here", and hangs until the test times out if the stream never ends. package:test offers **stream matchers** instead: matchers that consume events one by one and describe exactly what was expected when they fail. ## The core matchers Suppose a date-parsing library exposes `Stream<DateTime> parseLines(Stream<String> lines)`, which emits one date per valid line and an error event for each invalid line. | Matcher | Matches | |---|---| | `emits(m)` | one data event matching `m` (a value or a matcher) | | `emitsError(m)` | one error event whose error matches `m` | | `emitsDone` | the done event: the stream closed | | `emitsInOrder([..])` | each matcher in turn | | `emitsThrough(m)` | any events, then one matching `m` | | `mayEmit(m)` | one matching event if present, otherwise nothing | | `emitsAnyOf([..])` | one of several alternatives | | `neverEmits(m)` | the stream closes without an event matching `m` | | `emitsInAnyOrder([..])` | the matchers, in any order | ```dart test('reports a bad line and keeps going', () async { final lines = Stream.fromIterable(['2026-01-05', 'nope', '2026-02-10']); await expectLater( parseLines(lines), emitsInOrder([ DateTime(2026, 1, 5), emitsError(isA<FormatException>()), DateTime(2026, 2, 10), emitsDone, ]), ); }); ``` Values such as `DateTime(2026, 1, 5)` are wrapped in `equals` automatically, just as with `expect`. ## Extra events and emitsDone By default a stream matcher **allows more events after it finishes matching**. `expect(stream, emits(1))` passes for a stream that emits `1, 2, 3`. To assert that nothing else follows, end the sequence with `emitsDone`. This is the most common gap in stream tests: the check passes while the stream emits a stray duplicate afterwards. ## Asynchronous by nature Stream matchers are asynchronous. With `expect`, the call returns at once and the runner keeps the test open until the matcher decides; with `await expectLater`, the test body waits. `emitsInOrder` consumes **no** events if it fails, and `emitsThrough` fails without consuming if the stream closes first. ## Checking step by step with StreamQueue A single-subscription `Stream` can be listened to **once**. Running two `expectLater` calls on the same stream listens twice and fails. The fix is `StreamQueue` from `package:async`, which buffers a stream and hands out events on demand. Stream matchers accept a `StreamQueue` and **consume only the events they matched**, so a test can interleave assertions with actions: ```dart final queue = StreamQueue(parser.dates); parser.add('2026-03-01'); await expectLater(queue, emits(DateTime(2026, 3, 1))); parser.add('not a date'); await expectLater(queue, emitsError(isA<FormatException>())); await parser.close(); await expectLater(queue, emitsDone); ``` This is how you test a stream driven by method calls, such as a controller-backed parser, without collecting everything first. ## Pitfalls 1. **`toList()` on an endless stream**: the test hangs until the runner's timeout. 2. **Forgetting `emitsDone`**: extra events go unnoticed. 3. **Listening twice** to a single-subscription stream: use a `StreamQueue`, or a broadcast stream when that matches production. 4. **Checking an error with `emits(isA<FormatException>())`**: `emits` matches data events; an error event needs `emitsError`. 5. **Not awaiting** before the next action, so the event arrives after the test has moved on. ## Picking the right matcher quickly - The stream must produce exactly these events and end: `emitsInOrder([..., emitsDone])`. - The stream produces noise before the event you care about: `emitsThrough(x)`. - An optional heartbeat may or may not appear first: `mayEmit(heartbeat)` then the real matcher. - The order of a batch is not guaranteed: `emitsInAnyOrder([...])`. - Something must never happen before the stream closes: `neverEmits(x)`. ## A note on time Streams driven by timers, such as a parser that batches lines every 500 milliseconds, make stream tests slow and occasionally flaky if they wait in real time. Combining stream matchers with a fake clock keeps them instant; the matchers themselves do not care whether time is real or simulated, only about the order of events they receive.

  • Why does expect(stream, emits(1)) pass for a stream that emits 1, 2, 3?
    Stream matchers stop consuming once they have matched and allow further events by default. `emits(1)` is satisfied by the first event. To require that the stream ends there, use `emitsInOrder([1, emitsDone])`.
  • How do you assert that a stream never emits a particular value?
    Use `neverEmits(matcher)`. It consumes events until the stream closes and fails if any event matches. It needs the stream to close, so pair it with a stream that finishes or the test waits until the timeout.

A stream matcher is like a ticket inspector walking down a queue: it checks each passenger in turn and, unless told the queue must end, does not care who stands behind the last one it checked.

saying these in an interview costs you the question

  • Believes emits(x) fails if the stream emits more events afterwards
  • Uses emits(isA<FormatException>()) to match an error event
  • Calls toList() on a stream that never closes
  • Runs two expectLater calls on one single-subscription stream
  • Thinks stream matchers are synchronous and need no await