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?

1

There are 1 answers

4
Picci On BEST ANSWER

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.