How to test a stream in Dart

If your stream is emitting objects with properties you'd like to test, expectAsync1 can help:

List<Record> expectedRecords = [record1, record2, record3];
int i = 0;
recordStream.listen(
    expectAsync1<void,Record>(
    (record) {
      expect(record.name, expectedRecords[i].name);
      i++;
    }, 
    max: -1)
);

In the above example, expectAsync1 encloses an anonymous function:

    (record) {
      expect(record.name, expectedRecords[i].name);
      i++;
    }

This gets run each time a Record is emitted by the Stream recordStream

The 1 in expectAsync1 is the number of arguments your enclosed function will take. Most often, this would be 1. (record) is the one argument above.

For the above example, expectAsync1 has (optional) type arguments: <void,Record> The 2nd type argument Record tells the enclosed function that the stream item emitted is of Record type, allowing me to use properties like record.name without casting.

The 1st type argument is the return type of your enclosed function. I used void cause the enclosed function isn't returning anything, it's just running an expect Matcher and iterating a counter, which is used to iterate through the list of Record I'm expecting to see (i.e. expectedRecords) in that order.

Max argument

You'll notice the max: -1 below the enclosed function. That's an optional but important argument for expectAsync1 specifying the number of stream items/events we're expecting.

This defaults to 1 if max is not given and your test will fail if more than 1 event is emitted.

The error will be Callback called more times than expected (1).

In the example above I used -1 wich means unlimited events can be emitted/tested. You can specify a non-zero number if you want to test you get exactly that many items/events from your stream, else the test will fail. I could have used max: 3 for my example above.

RxDart

If you're using RxDart BehaviorSubject remember the most recent stream event is emitted upon listen. So in your test, when you start listening / using expectAsync1 there will be an immediate call of the enclosed function with the most recent event.

ReplaySubject will emit all previous stream events upon listen.


Try using async/await and expectLater

test('words are reading sequentially correct', () async {
  WordTrackerInterface wordTracker = WordTracker.byContent('0 1 2');
  wordTracker.setWordsCountPerChunk(1);
  var stream = wordTracker.nextWordStringStream();

  await expectLater(
      stream,
      emitsInOrder(List<Word>.generate(
          6, (i) => i > 2 ? Word('') : Word(i.toString()))));

  for (int i = 0; i < 6; i++) {
    wordTracker.nextWord();
  }
});

After reading the source code in dart files and reading on the internet, I found the solution: I needed to create a custom Matcher. I tested the following code on my laptop and by referencing other files from my application like 'WordTracker', the code ran as expected.

test('words are reading sequentially correct', () {
    WordTrackerInterface wordTracker = WordTracker.byContent('0 1 2');
    wordTracker.setWordsCountPerChunk(1);
    var stream = wordTracker.nextWordStringStream();

    expect(stream, 
      emitsInOrder(List<Word>.generate(6, (i) => i > 2 ? Word('') : Word(i.toString())).map<WordMatcher>(
        (Word value) => WordMatcher(value))));

    for (int i = 0; i < 6; i++) {
      wordTracker.nextWord();
    }
  });


class WordMatcher extends Matcher {
  Word expected;
  Word actual;
  WordMatcher(this.expected);

  @override
  Description describe(Description description) {
    return description.add("has expected word content = '${expected.content}'");
  }

  @override
  Description describeMismatch(
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
    bool verbose
  ) {
    return mismatchDescription.add("has actual emitted word content = '${matchState['actual'].content}'");
  }

  @override
  bool matches(actual, Map matchState) {
    this.actual = actual;
    matchState['actual'] = actual is Word ? actual : Word('unknown');
    return (actual as Word).content == expected.content;
  }
}