Rxdart: Testing of `BehaviorSubject` stream fails with non-primitive types

Created on 2 Jul 2018  Â·  2Comments  Â·  Source: ReactiveX/rxdart

Here's a weird one, I hope I'm not making some stupid mistake. Having the first test of BehaviorSubject:

test('emits the most recently emitted item to every subscriber', () async {
  // ignore: close_sinks
  final StreamController<int> subject = new BehaviorSubject<int>();

  subject.add(1);
  subject.add(2);
  subject.add(3);

  await expectLater(subject.stream, emits(3));
  await expectLater(subject.stream, emits(3));
  await expectLater(subject.stream, emits(3));
});

it will succeed. However, when I do the same test but with simple classes, it will fail:

test("emits the most recently emitted object to every subscriber", () async {
  final subject = BehaviorSubject<SimpleClass>();

  subject.add(SimpleClass(0));
  subject.add(SimpleClass(1));
  subject.add(SimpleClass(2));

  await expectLater(subject.stream, emits(SimpleClass(0)));
  await expectLater(subject.stream, emits(SimpleClass(1)));
  await expectLater(subject.stream, emits(SimpleClass(2)));
});

for

class SimpleClass {
  final int val;

  SimpleClass(this.val);

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is SimpleClass &&
          runtimeType == other.runtimeType &&
          val == other.val;

  @override
  int get hashCode => val.hashCode;
}

with an error

Expected: should emit an event that <Instance of 'SimpleClass'>
  Actual: <Instance of 'BehaviorSubject<SimpleClass>'>
   Which: emitted • Instance of 'SimpleClass'

This is for Flutter project with Dart 2 (I think):

â–¶ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel dev, v0.5.5, on Mac OS X 10.13.5 17F77, locale en-PL)
[✓] Android toolchain - develop for Android devices (Android SDK 28.0.0-rc1)
[✓] iOS toolchain - develop for iOS devices (Xcode 9.4.1)

Same thing on Beta channel. emits(equals(...)) also doesn't work. Should I not use expect..emits with BehaviorSubjects?

Most helpful comment

Hey hey, thanks for writing in! In this case, the BehaviorSubject is working as intended. So what's happening? You're adding subject.add(SimpleClass(2)); last, and BehaviorSubject will replay the last added value to new listeners!

Since you're listening after you add the value, it's replaying the SimpleClass(2) to each listener, which is why this line fails: await expectLater(subject.stream, emits(SimpleClass(0))); It might be easier to see this in action if you also implement toString on your class:

Expected: should emit an event that SimpleClass:<SimpleClass{val: 0}>
  Actual: <Instance of 'BehaviorSubject<SimpleClass>'>
   Which: emitted • SimpleClass{val: 2}

This is also why the first test works! If you were to change the expectations to this, the test would start failing:

  await expectLater(subject.stream, emits(1));
  await expectLater(subject.stream, emits(2));
  await expectLater(subject.stream, emits(3));

So how do you test this kinda thing? The important thing to understand is that you need to add data to the Subject after you start listening. I'd also recommend using emitsInOrder.

You can do it one of two ways:

  1. Use expect before you add data
    test("emits the most recently emitted object to every subscriber", () {
      final BehaviorSubject<SimpleClass> subject =
          new BehaviorSubject<SimpleClass>();

      expect(
        subject.stream,
        emitsInOrder(
          <SimpleClass>[
            new SimpleClass(0),
            new SimpleClass(1),
            new SimpleClass(2)
          ],
        ),
      );

      subject.add(new SimpleClass(0));
      subject.add(new SimpleClass(1));
      subject.add(new SimpleClass(2));
    });
  1. Schedule the add calls to happen async using scheduleMicrotask. I kinda like this since it follows the "arrange, act, assert" principle
    test("emits the most recently emitted object to every subscriber", () {
      final BehaviorSubject<SimpleClass> subject =
          new BehaviorSubject<SimpleClass>();

      scheduleMicrotask(() {
        subject.add(new SimpleClass(0));
        subject.add(new SimpleClass(1));
        subject.add(new SimpleClass(2));
      });

      expect(
        subject.stream,
        emitsInOrder(
          <SimpleClass>[
            new SimpleClass(0),
            new SimpleClass(1),
            new SimpleClass(2)
          ],
        ),
      );
    });

Hope this helps!

All 2 comments

Hey hey, thanks for writing in! In this case, the BehaviorSubject is working as intended. So what's happening? You're adding subject.add(SimpleClass(2)); last, and BehaviorSubject will replay the last added value to new listeners!

Since you're listening after you add the value, it's replaying the SimpleClass(2) to each listener, which is why this line fails: await expectLater(subject.stream, emits(SimpleClass(0))); It might be easier to see this in action if you also implement toString on your class:

Expected: should emit an event that SimpleClass:<SimpleClass{val: 0}>
  Actual: <Instance of 'BehaviorSubject<SimpleClass>'>
   Which: emitted • SimpleClass{val: 2}

This is also why the first test works! If you were to change the expectations to this, the test would start failing:

  await expectLater(subject.stream, emits(1));
  await expectLater(subject.stream, emits(2));
  await expectLater(subject.stream, emits(3));

So how do you test this kinda thing? The important thing to understand is that you need to add data to the Subject after you start listening. I'd also recommend using emitsInOrder.

You can do it one of two ways:

  1. Use expect before you add data
    test("emits the most recently emitted object to every subscriber", () {
      final BehaviorSubject<SimpleClass> subject =
          new BehaviorSubject<SimpleClass>();

      expect(
        subject.stream,
        emitsInOrder(
          <SimpleClass>[
            new SimpleClass(0),
            new SimpleClass(1),
            new SimpleClass(2)
          ],
        ),
      );

      subject.add(new SimpleClass(0));
      subject.add(new SimpleClass(1));
      subject.add(new SimpleClass(2));
    });
  1. Schedule the add calls to happen async using scheduleMicrotask. I kinda like this since it follows the "arrange, act, assert" principle
    test("emits the most recently emitted object to every subscriber", () {
      final BehaviorSubject<SimpleClass> subject =
          new BehaviorSubject<SimpleClass>();

      scheduleMicrotask(() {
        subject.add(new SimpleClass(0));
        subject.add(new SimpleClass(1));
        subject.add(new SimpleClass(2));
      });

      expect(
        subject.stream,
        emitsInOrder(
          <SimpleClass>[
            new SimpleClass(0),
            new SimpleClass(1),
            new SimpleClass(2)
          ],
        ),
      );
    });

Hope this helps!

Aaand of course that was the stupid mistake I was making :) Before that I was confused as to scheduling, I think -- this issue was just a 'repro' I thought I did -- time to stop coding for today, I guess.

I'm glad I asked the question though, scheduleMicrotask is a great tip. Also amazing response time, thanks a lot!

Was this page helpful?
0 / 5 - 0 ratings