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:

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!

--

--

Anna Leushchenko πŸ‘©β€πŸ’»πŸ’™πŸ“±πŸ‡ΊπŸ‡¦

Google Developer Expert in Dart and Flutter | Author, speaker at tech events, mentor, OSS contributor | Passionate mobile apps creator