Testing Asynchronous Code and Streams
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
asyncwhen you need toawaitinside them. - Use
expectLater()(returns aFuture) instead ofexpect()for matchers that themselves resolve asynchronously. - Call
awaiton the result ofexpectLater()so the test waits for the assertion to complete.
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>()),
);
});
}
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 withemits()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.
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;
});
}
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
asyncandawaitall async assertions to prevent false passes. - Use
await+ direct assertion for simple futures; usecompletion()withexpectLater()for more expressive future assertions. - Use
throwsA()to verify that futures reject with the correct error type. - Use
emitsInOrder()withexpectLater()to verify the full sequence of stream events including errors and the done event. - Clean up stream controllers in
tearDownto keep your test suite reliable.