2 things you really need to know to test streams in Flutter

14 Oct 2022

flutter logo image

Streams are a great feature of Dart and many Flutter apps make use of them. But if you do use them in your Flutter apps you will also want to test them and for that, I present here two things that I've found essential to know in testing streams.

1. Stream Matchers

The first thing is using Stream matchers in your tests. You may already be familiar with using matchers in your tests, for example:

import 'package:test/test.dart';

void main() {
  test('basic test', () async {
    const lifeTheUniverseAndEverything = 21 + 21;
    expect(lifeTheUniverseAndEverything, equals(42));
  });
}

In the above, the value of equals(42) returns a matcher.

So how do we use matchers with streams? Well if for instance we have a very basic stream that emits a single int value, we might want to test that it actually emits the int we expect and for this we can use emitsInOrder() function to get a StreamMatcher.

  test('test a stream emits expected item', () async {
    final testSubject = StreamController<int>();

    testSubject.add(1);

    expect(testSubject.stream, emitsInOrder([1]));
  });
}

And yes if we run that we get a passing test.

As the example code in the documentation for StreamMatcher notes, we can actually do much more complex matching:

test('test a stream emits expected items in expected order', () async {
    final testSubject = StreamController<int>();

    testSubject.add(1);
    testSubject.add(2);
    testSubject.add(3);
    testSubject.close();

    expect(
      testSubject.stream,
      emitsInOrder(
        [
          1,
          emitsAnyOf([0, 2]),
          lessThanOrEqualTo(3),
          emitsDone,
        ],
      ),
    );
  });

I highly recommend reading Andrea's excellent article that has a much more detailed explanation of using matchers for testing streams.

2. The Q

The second important tip I have when it comes to testing streams, is how to go about testing code that listens to a stream Imagine we have a class that takes a stream as a dependency and then does some processing on the stream events and then may change its state based on that. In more concrete terms, we may have:

class Marvin {
  final Stream<int> answerStream;

  bool isHappy = false;

  Marvin(this.answerStream) {
    answerStream.listen((event) {
      isHappy = (event == 42);
    });
  }
}

and hence we may devise a test like so:

test('test Marvin is happy with the correct answer', () async {
    final fakeStream = StreamController<int>();
    final testMarvin = Marvin(fakeStream.stream);

    fakeStream.add(42);

    expect(testMarvin.isHappy, true);
  });

But sadly when we run the above test we get:

Expected: <true>
  Actual: <false>

package:test_api                          expect
main.<fn>
test/dart_streams_testing_test.dart:46
2

✖ test Marvin is happy with the correct answer

What went wrong?

Well we have forgotten to take into account is how in Dart streams are processed and hence how their events are delivered to listeners. While its not explicitly spelled out, in the excellent article about async programming in Dart its says:

All of the high-level APIs and language features that Dart has for asynchronous programming — futures, streams, async and await — they’re all built on and around this simple loop.

A consequence of the above is that stream events are actually processed, aka delivered to their listeners via the eventloop. Hence if we want to test for the delivery of the items we added to our test stream by our testMarvin listener, we need to allow for the eventloop to complete and hence do the processing of stream events that happens at the end of each go around the loop. So how can we do this? Well luckily for us, the Dart team is one step ahead of us, and has provided for this very contingency with the pumpEventQueue() function. So with this addition to our test:

test('test Marvin is happy with the correct answer', () async {
    final fakeStream = StreamController<int>();
    final testMarvin = Marvin(fakeStream.stream);

    fakeStream.add(42);

    await pumpEventQueue();

    expect(testMarvin.isHappy, true);
  });

Our test passes as expected.

Stream on!

As you can see, testing Streams in Dart and hence your Flutter apps need not be a difficult task, as long as you know these two very useful pieces of knowledge. Happy coding!