Testing Flutter Applications

Testing Asynchronous Code and Streams

16 min Lesson 5 of 12

Testing Asynchronous Code and Streams

Real-world Flutter applications are full of asynchronous operations: fetching data from an API, reading from local storage, authenticating users, and listening to real-time data streams. Writing tests that correctly verify this async behaviour requires dedicated tools provided by the flutter_test package. In this lesson you will learn to test Future-based code with async/await and the completion() matcher, and to test Stream-based code with expectLater() and the emitsInOrder() matcher.

Why Async Testing Is Different

Synchronous tests finish their assertions before the test function returns. Asynchronous tests must wait for futures to resolve or streams to emit values before any assertion can be made. If you forget to await an async expectation, the test will pass trivially — the assertions never run — giving you a false sense of security.

  • Always mark test callbacks async when you need to await inside them.
  • Use expectLater() (returns a Future) instead of expect() for matchers that themselves resolve asynchronously.
  • Call await on the result of expectLater() so the test waits for the assertion to complete.
Note: expectLater() is identical to expect() except it returns a Future<void>. Always await it; otherwise the test may exit before the matcher finishes evaluating.

Testing Futures with async/await and completion()

The simplest approach to testing a Future is to await its result and then run a normal synchronous assertion:

Awaiting a Future directly

import 'package:flutter_test/flutter_test.dart';

Future<String> fetchGreeting(String name) async {
  await Future.delayed(const Duration(milliseconds: 50));
  return 'Hello, $name!';
}

void main() {
  test('fetchGreeting returns correct message', () async {
    final result = await fetchGreeting('Edrees');
    expect(result, equals('Hello, Edrees!'));
  });
}

For cases where you want to assert only that a Future completes successfully (without caring about the exact value), use the completion() matcher together with expectLater():

Using completion() and expectLater()

import 'package:flutter_test/flutter_test.dart';

Future<int> computeSquare(int n) async {
  await Future.delayed(const Duration(milliseconds: 10));
  return n * n;
}

void main() {
  test('computeSquare(5) completes with 25', () async {
    // completion() wraps any matcher and asserts the future resolves to it
    await expectLater(
      computeSquare(5),
      completion(equals(25)),
    );
  });

  test('computeSquare completes without throwing', () async {
    // completes is a convenience matcher — succeeds if the future resolves
    await expectLater(computeSquare(3), completes);
  });

  test('an invalid operation throws', () async {
    // throwsA checks that the future rejects with a given error type
    await expectLater(
      Future<void>.error(ArgumentError('bad input')),
      throwsA(isA<ArgumentError>()),
    );
  });
}
Tip: Use completion(matcher) when the future's resolved value matters. Use completes when you only care that it does not throw. Use throwsA(isA<ExceptionType>()) to assert expected error paths.

Testing Streams with expectLater() and emitsInOrder()

Streams emit zero or more values over time before optionally completing or erroring. The emitsInOrder() matcher lets you declare the full sequence of expected events and verify them in order:

Testing a Stream sequence

import 'package:flutter_test/flutter_test.dart';

Stream<int> countDown(int from) async* {
  for (var i = from; i >= 1; i--) {
    await Future.delayed(const Duration(milliseconds: 10));
    yield i;
  }
}

void main() {
  test('countDown(3) emits 3, 2, 1 in order then closes', () async {
    await expectLater(
      countDown(3),
      emitsInOrder([3, 2, 1, emitsDone]),
    );
  });

  test('stream emits an error', () async {
    final errorStream = Stream<int>.error(StateError('broken'));
    await expectLater(
      errorStream,
      emitsError(isA<StateError>()),
    );
  });
}

Key Stream Matchers

  • emitsInOrder([...]) — asserts each value is emitted in the listed order; wrap individual values with emits() or use raw values (auto-wrapped).
  • emits(value) — asserts the stream emits one specific value next.
  • emitsError(matcher) — asserts the stream emits an error matching the given matcher.
  • emitsDone — asserts the stream closes (done event) with no more values.
  • neverEmits(matcher) — asserts the stream never emits a value matching the matcher before closing.
  • mayEmit(matcher) — optionally matches a value; succeeds whether or not the value is emitted.
Warning: Do not forget emitsDone at the end of an emitsInOrder list if you want to assert that the stream closes after the expected values. Omitting it means extra unexpected events will not be caught.

Testing a Repository That Exposes a Stream

In practice you will often test a service or repository class rather than a raw stream. The pattern is the same — mock dependencies, invoke the method, assert on the returned stream:

Repository stream test pattern

import 'package:flutter_test/flutter_test.dart';

// Minimal fake stream-based counter service
class CounterService {
  final _controller = StreamController<int>.broadcast();

  Stream<int> get counterStream => _controller.stream;

  void increment(int current) => _controller.add(current + 1);
  void dispose() => _controller.close();
}

void main() {
  late CounterService service;

  setUp(() => service = CounterService());
  tearDown(() => service.dispose());

  test('counterStream emits incremented values', () async {
    final future = expectLater(
      service.counterStream,
      emitsInOrder([1, 2, 3]),
    );

    service.increment(0);
    service.increment(1);
    service.increment(2);

    await future;
  });
}
Note: Always call tearDown to close stream controllers. Leaving them open causes resource leaks and can cause subsequent tests to fail with unexpected events.

Summary

Testing asynchronous Dart code requires understanding a small but important set of rules and matchers:

  • Mark test callbacks async and await all async assertions to prevent false passes.
  • Use await + direct assertion for simple futures; use completion() with expectLater() for more expressive future assertions.
  • Use throwsA() to verify that futures reject with the correct error type.
  • Use emitsInOrder() with expectLater() to verify the full sequence of stream events including errors and the done event.
  • Clean up stream controllers in tearDown to keep your test suite reliable.