Search code examples
angulartypescriptunit-testingrxjsrxjs-marbles

How to test that a series of function calls on a service produce the expected series of emitted values from an Observable on that same service


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?


Solution

  • 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.