testingrxjsjasmine-marbles

RxJs Marble testing concatMap with withLatestFrom


How can be unit tested this Observable?

e1.pipe(
    concatMap(x => of(x).pipe(withLatestFrom(e2)))
);

Following unit test fails:

        it('test', () => {
            const e1 = hot(       '--1^---2----3-|');
            const e2 = hot(       '-a-^-b-----c--|');
            const expected = cold(   '----x----y-|', {
                x: ['2', 'b'],
                y: ['3', 'c']
            });

            const result = e1.pipe(
                concatMap(x => of(x).pipe(
                    withLatestFrom(e2))
                )
            );

            // but this works:
            // const result = e1.pipe(withLatestFrom(e2));

            expect(result).toBeObservable(expected);
        });

How the marbles should be written in order to pass this unit test? What did I do wrong? I expect by inserting concatMap operator in the chain (before withLatestFrom) I have to also somehow "mark" it in the marbles.


Solution

  • In your real example

    e1.pipe(
      concatMap(x => of(x).pipe(withLatestFrom(e2)))
    );
    

    everything works fine probably because is either a BehaviorSubject or a ReplaySubject, which it's not case in your test.

    Although you're using hot( '-a-^-b-----c--|');, it does not imply that you're using a BehaviorSubject. If we look at the implementation, we'll see that HotObservable extends the Subject class:

    export class HotObservable<T> extends Subject<T> implements SubscriptionLoggable { /* ... */ }
    

    which should help understand why this works:

    const result = e1.pipe(withLatestFrom(e2));
    

    and this doesn't:

    const result = e1.pipe(
        concatMap(x => of(x).pipe(
            withLatestFrom(e2))
        )
    );
    

    In the first snippet, e2 is subscribed when e1 is subscribed. In the second one, because you're using concatMap, every time e1 emits, withLatestFrom(e2)) will be subscribed and then unsubscribed, due to the complete notification that comes from of(x).

    With this in mind, here would be my approach:

    Note: I'm using the built-in functions provided by rxjs/testing

    it('test', () => {
    
      // might want to add this in a `beforeEach` function
      let testScheduler = new TestScheduler(
        (actual, expected) => (console.log({actual, expected}),expect(actual).toEqual(expected))
      );
    
      testScheduler.run(({ hot, expectObservable }) => {
        const e1 = hot(       '--1^---2----3-|');
        const e2src = hot(       '-a-^-b-----c--|');
        const e2 = new BehaviorSubject(undefined);
    
        const result = e1.pipe(
            concatMap(x => of(x).pipe(
                withLatestFrom(e2))
            )
        );
    
        const source = merge(
          result,
    
          e2src.pipe(
            tap(value => e2.next(value)),
            
            // this is important as we're not interesting in `e2src`'s values
            // it's just a way to `feed` the `e2` BehaviorSubject
            ignoreElements()
          )
        );
        
        expectObservable(source).toBe('----x----y-|', {
          x: ['2', 'b'],
          y: ['3', 'c']
        });
      });
    })