1

I have the following service:

export class MathService {
  private _total = new BehaviorSubject(0);
  total$ = this._total.asObservable();

  add(num: number) {
    this._total.next(this._total.value() + num);
  }

  subtract(num: number) {
    this._total.next(this._total.value() - num);
  }
}

How would you test that total$ emits the correct values in a sequence of add and subtract function calls like this:

  service.add(10) // should emit 10;
  service.subtract(3) // should emit 7;
  service.add(20) // should emit 27;
  service.subtract(5) // should emit 22;
  ...

Would marble test work for something like this? If so, how would you set that up? I wasn't able to find a clear example online of how to test that an observable on a service emits the proper sequence of values given a sequence of function calls on that service?

snowfrogdev
  • 5,963
  • 3
  • 31
  • 58
  • 1
    It really depends on how you want to test it and if you know that in test (or mocks) you can guarantee that it's always going to be synchronous. Then you can subscribe to `total$` and collect everything it receives. It also depends on if you want to use classic mock/jest expect calls or RxJS marble testing. You can check how Angular tests Observables internally https://github.com/angular/angular/blob/master/packages/router/test/integration.spec.ts#L2081-L2106 – martin Mar 17 '22 at 12:29
  • Right, subscribing to the observable and collecting the events in an array then checking that the elements in the array are the ones you expect to see. I guess that works fine. I'd love to see an example of how to do this with marble tests though, if it's even possible. – snowfrogdev Mar 17 '22 at 12:45

1 Answers1

2

First of all I would try to test without marble diagrams, just to make sure we understand how the asyn execution would work.

it('should test the Observable', () => {
    // create the instance of the service to use in the test
    const mathService = new MathService();
    // define the constant where we hold the notifications
    const result: number[] = [];
    const expected = [0, 0, 0, 1, 0, 2, 0];  // expected notifications

    // this is a sequence of adds
    const add$ = timer(0, 100).pipe(
        take(3),
        tap((i) => {
            console.log('add', i);
            mathService.add(i);
        }),
    );

    // this is a sequence of subtracts, which starts 50 ms after the adds
    const subtract$ = timer(50, 100).pipe(
        take(3),
        tap((i) => {
            console.log('sub', i);
            mathService.subtract(i);
        }),
    );

    // here we subscribe to total$ and we store any notification in the result array
    mathService.total$.subscribe({
        next: (s) => {
            result.push(s);
        },
    });

    // here we merge adds and subtracts and, at completion, we check which are the notifications
    // we have saved in the result array
    merge(add$, subtract$).subscribe({
        complete: () => {
            console.log('===>>>', result, expected);
        },
    });
});

Once the async mechanism is clear, then we can look at an implementation which uses marble diagrams, like this one

let testScheduler: TestScheduler;

beforeEach(() => {
    testScheduler = new TestScheduler(observableMatcher);
});

it.only('should test the Observable', () => {
    testScheduler.run(({ hot, expectObservable }) => {
        const mathService = new MathService();

        const add = hot('        --0-----1---2---------');
        const subtract = hot('   ----0-----1---2-------');
        const expected = '       --0-0---1-0-2-0-------';

        const _add = add.pipe(tap((i) => mathService.add(parseInt(i))));
        const _subtract = subtract.pipe(tap((i) => mathService.subtract(parseInt(i))));
        const result = merge(_add, _subtract).pipe(
            concatMap((val) => {
                console.log('val', val);
                return mathService.total$.pipe(map((v) => v.toString()));
            }),
        );

        expectObservable(result).toBe(expected);
    });
});

This implementation follows some examples of tests used in the rxJs library.

The implmentation of observableMatcher can be seen here.

Picci
  • 16,775
  • 13
  • 70
  • 113
  • Thanks for this. It has been very helpful. I've run into a small problem when reproducing your technique for the marble test though. Everything works fine when the add and subtract stream fire on separate ticks, but when I try to add concurrent events, the test starts failing with unexpected values. You wouldn't happen to know why? Could it be because of how merge works? – snowfrogdev Mar 21 '22 at 11:00
  • Can you share a stackblitz with the problem you are encountering? – Picci Mar 21 '22 at 11:11
  • Sure, here you go. You'll notice that the SUT is slightly different than the one in my question, but similar enough. https://stackblitz.com/edit/rxjs-d4qeys?file=index.ts – snowfrogdev Mar 21 '22 at 12:15
  • 1
    What you have is a peculiar situation, at least for marbles. This is [the version of your stackblitz](https://stackblitz.com/edit/rxjs-cgt27q?file=index.ts) adjusted to work with marbles that have notifications which occur in the same frame. You may want to read the marble [syntax documentation](https://rxjs.dev/guide/testing/marble-testing#marble-syntax) for more details but the short story is that you have `process1` and `process2` which notify in the same frame and therefore the `expected` has to specify that it expects more than one notification in the same frame using parenthesis. – Picci Mar 21 '22 at 16:53