felangel / mocktail

A mock library for Dart inspired by mockito
https://pub.dev/packages/mocktail
MIT License
617 stars 81 forks source link

question: how do I mock a stream's `listen` method? #145

Closed mugbug closed 2 years ago

mugbug commented 2 years ago

Let's say I have the following implementation:

class Foo {
  late StreamSubscription _subscription;

  Foo({required Bar bar, required EventTracker tracker}) {
    _subscription = bar.stream.listen((event) {
      tracker.recordEvent(event);
    });
  }
}

class Bar {
  final StreamController<int> _streamController = StreamController<int>();

  Stream<int> get stream => _streamController.stream;
}

class EventTracker {
  void recordEvent(int value) {
    // track
  }
}

And I want to test if when Bar's stream emits some events after Foo is instantiated, all of those are recorded on a EventTracker.

Here's what I currently have, that works just for the first event:

class MockBar extends Mock implements Bar {}
class MockEventTracker extends Mock implements EventTracker {}

void main() {
  final mockBar = MockBar();
  final mockEventTracker = MockEventTracker();
  late Foo foo;

  setUp(() {
    when(
      () => mockBar.stream,
    ).thenAnswer((_) => Stream<int>.fromIterable([7, 3, 2, 5]));
    foo = Foo(bar: mockBar, tracker: mockEventTracker);
  });

  test('should record all events sent by stream', () {
    final captured = verify(
      () => mockEventTracker.recordEvent(captureAny()),
    ).captured; // returns [7]

    expect(captured[0], 7); // just this expectation passes
    expect(captured[1], 3);
    expect(captured[2], 2);
    expect(captured[3], 5);
  });
}

Am I mocking this in the correct way? Any idea why only the first event is emitted/captured?

micaelcid commented 2 years ago

You have to listen mockBar.stream in your test and then call verify and expected inside the listen callback:

void main() {
  final StreamController<int> controllerMock = StreamController<int>();
  final mockBar = MockBar();
  final mockEventTracker = MockEventTracker();
  final values = [7, 3, 2, 5];

  late Foo foo;

  setUp(() {
    when(
      () => mockBar.stream,
    ).thenAnswer((_) => Stream<int>.fromIterable(values));
    foo = Foo(bar: mockBar, tracker: mockEventTracker);
  });

  test('should record all events sent by stream', () {
    int index = 0;
    mockBar.stream.listen((event) {
      expect(event, values[index]);
      verify(
        () => mockEventTracker.recordEvent(event),
      ).called(1);
      index++;
    });
  });
}
mugbug commented 2 years ago

[SOLVED]

Ok, so I was able to solve this in a reasonable way: basically, it seems the problem was related to asynchronicity. When verify was called, the stream didn't have finished emitting all events. So to make it wait so all events were emitted before verifying the calls, I implemented this method:

Future<void> waitForStreamToComplete(Stream stream) {
  final complete = Completer<void>();
  stream.listen((event) {}).onDone(() {
    complete.complete();
  });
  return complete.future;
}

which can be called before the verify:

test('should record all events sent by the stream', () async {
    // make sure the stream is done emitting events
    await waitForStreamToComplete(mockBar.stream);

    // proceed with the test assertions
    final captured = verify(
      () => mockEventTracker.recordEvent(captureAny()),
    ).captured; // returns [7, 3, 2, 5]

    // all expectations should be satisfied
    expect(captured[0], 7);
    expect(captured[1], 3);
    expect(captured[2], 2);
    expect(captured[3], 5);
  });