Assertions in Dart and Flutter tests: asynchronous expect and matchers
This is the part of the ultimate cheat sheet dedicated to:
- asynchronous expect with
expectLater()
, - future matchers,
- stream matchers.
In this series:
- expect and matcher
- equality matchers
- type and error matchers
- collection matchers
- numeric and comparable matchers
- universal and custom matchers
- matcher operators
- asynchronous expect and matchers
- Flutter widget matchers
- accessibility matchers
- golden matchers
- mock invocations and parameters
Asynchronous expect
The expectLater()
function is just like expect()
, but returns a Future
that completes when the matcher has finished matching.
Future<void> expectLater(
dynamic actual, // actual value to be verified
dynamic matcher, { // characterises the expected result
String? reason, // added to the output in case of failure
dynamic skip, // true or a String with the reason to skip
}) {...}
While expectLater()
can accept any matcher, it makes sense to pass children of AsyncMatcher
class, which does asynchronous computation.
Future matchers
There are a few matchers to test Future
execution results.
The completes
matcher completes successfully when the Future
completes successfully with any value.
test('expectLater: completes β
', () async {
final result = Future.value(0);
await expectLater(result, completes);
});
The completion
matcher accepts the matcher to verify theFuture
result:
test('expectLater: completion β
', () async {
final result = Future.value(0);
await expectLater(result, completion(isZero));
});
And the throwsA
matcher should be already familiar:
test('expectLater: throwsA β
', () async {
final result = Future.error(Exception());
await expectLater(result, throwsA(isException));
});
Stream matchers
First, weβll focus on testing streams with hardcoded values to see the variety of stream matchers. And then, weβll have a word about testing streams when itβs too late to use the expect()
function.
emits
/ neverEmits
The emits
matcher checks that the Stream has emitted a value that satisfies a matcher, that emits
has accepted as a parameter. It may accept the expected value, another matcher that characterizes the expected value, or a predicate function:
test('expect: emits β
', () {
final stream = Stream.fromIterable([0]);
expect(stream, emits(0));
expect(stream, emits(isZero));
expect(stream, emits((value) => value == 0));
expect(stream, emits(predicate<int>((value) => value == 0)));
});
The neverEmits
matcher performs the opposite check:
test('expect: neverEmits β
', () {
final stream = Stream.fromIterable([1]);
expect(stream, neverEmits(0));
expect(stream, neverEmits(isZero));
expect(stream, neverEmits((value) => value == 0));
expect(stream, neverEmits(predicate<int>((value) => value == 0)));
});
emitsInOrder
/ emitsInAnyOrder
These matchers ensure a stream has emitted multiple events.
In particular order:
test('expect: emitsInOrder β
', () {
final stream = Stream.fromIterable([0, 1]);
expect(stream, emitsInOrder([isZero, 1]));
});
Or in no particular order:
test('expect: emitsInAnyOrder β
', () {
final stream = Stream.fromIterable([Result(0), Result(1)]);
expect(stream, emitsInAnyOrder([hasValue(1), Result(0)]));
});
As you see, both accept an array, containing expected values or matchers.
emitsDone
The emitsDone
matcher helps ensure a stream does not emit any more unexpected values:
test('expect: emitsDone β
', () {
final stream = Stream.empty();
expect(stream, emitsDone);
});
test('expect: emitsDone β
', () {
final stream = Stream.value(0);
expect(stream, emitsInOrder([0, emitsDone]));
});
emitsError
The emitsError
matcher helps ensure a stream has emitted an error, and accepts another matcher to verify the exact error:
test('expect: emitsError β
', () {
final stream = Stream.error(UnimplementedError());
expect(stream, emitsError(isUnimplementedError));
});
Testing closed / drained streams
So far we tested streams that contained hardcoded values, which were emitted immediately inside the expect()
function. But imagine, we have to test a stream that was already closed, or a stream that has already emitted values we are interested in.
Letβs take a look at this class:
class StreamExample {
final _streamController = StreamController<int>.broadcast();
void doWork() {
_streamController.add(0);
_streamController.add(1);
}
Stream<int> get stream => _streamController.stream;
}
When doWork()
method is called, the stream
should emit two values: 0
and 1
. Here is a test that comes to mind for this behavior:
test('expect: drained stream β', () async {
final streamExample = StreamExample();
streamExample.doWork();
expect(streamExample.stream, emitsInOrder([0, 1]));
});
Unfortunately, the expect()
function is called too late, emitted values are already gone, and this test never completes. Instead, expect()
or expectLater()
should be used before doWork()
call.
Unlike using expectLater()
with Future matchers, where it is placed at the end of the test and is awaited, for testing StreamMatcher
, it should be placed before performing the calls that affect stream values. This way, we can catch values as the stream emits them. In such a case, the expectLater()
call should not be awaited, otherwise, the test will not complete as well.
test('expectLater: drained stream β
', () async {
final streamExample = StreamExample();
expectLater(streamExample.stream, emitsInOrder([0, 1]));
streamExample.doWork();
});
Originally published at Invertase blog. Check out their awesome Authors Program!
Hi! ππ» Iβm Anna, Google Developer Expert in Flutter from Ukraine πΊπ¦ Follow me on Twitter, GitHub, YouTube, Medium to get notifications about my latest work.
Itβs early 2023, and we in Ukraine are still fighting against russians committing genocide on our lands. If you find this content useful and have a coin to spare, support us with your donations. Stand with Ukraine!