Search code examples
ngrx-effectsjasmine-marbles

How to properly use jasmine-marbles to test multiple actions in ofType


I have an Effect that is called each time it recives an action of more than one "kind"

myEffect.effect.ts

  someEffect$ = createEffect(() =>
    this.actions$.pipe(
      ofType(fromActions.actionOne, fromActions.actionTwo),
      exhaustMap(() => {
        return this.myService.getSomeDataViaHTTP().pipe(
          map((data) =>
            fromActions.successAction({ payload: data})
          ),
          catchError((err) =>
            ObservableOf(fromActions.failAction({ payload: err }))
          )
        );
      })
    )
  );

in my test I tried to "simulate the two different actions but I always end up with an error, while if I try with one single action it works perfectly

The Before Each part

describe('MyEffect', () => {
  let actions$: Observable<Action>;
  let effects: MyEffect;
  let userServiceSpy: jasmine.SpyObj<MyService>;
  const data = {
  // Some data structure
  };
  beforeEach(() => {
    const spy = jasmine.createSpyObj('MyService', [
      'getSomeDataViaHTTP',
    ]);
    TestBed.configureTestingModule({
      providers: [
        MyEffect,
        provideMockActions(() => actions$),
        {
          provide: MyService,
          useValue: spy,
        },
      ],
    });

    effects = TestBed.get(MyEffect);
    userServiceSpy = TestBed.get(MyService);
  });

This works perfectly

    it('should return successActionsuccessAction', () => {
    const action = actionOne();
    const outcome = successAction({ payload: data });

    actions$ = hot('-a', { a: action });
    const response = cold('-a|', { a: data });
    userServiceSpy.getSomeDataViaHTTP.and.returnValue(response);

    const expected = cold('--b', { b: outcome });
    expect(effects.someEffect$).toBeObservable(expected);
  });

This doesn't work

it('should return successAction', () => {
const actions = [actionOne(), actionTwo()];
const outcome = successAction({ payload: data });
actions$ = hot('-a-b', { a: actions[0], b: actions[1] });
const response = cold('-a-a', { a: data });
userServiceSpy.getSomeDataViaHTTP.and.returnValue(response);
const expected = cold('--b--b', { b: outcome });
expect(effects.someEffect$).toBeObservable(expected);
});

Solution

  • There are two problems in this code.

    1. It suggests that getSomeDataViaHTTP returns two values. This is wrong, the response is no different from your first example: '-a|'
    2. It expects the second successAction to appear after 40 ms (--b--b, count the number of dashes). This is not correct, because actionTwo happens after 20 ms (-a-a) and response takes another 10 ms (-a). So the first successAction is after 20ms (10+10), the second is after 30ms (20+10). The marble is: '--b-b'.
    Input actions     : -a -a
    1st http response :  -a
    2nd http response :     -a
    Output actions    : --b -b
    

    The working code:

    it('should return successAction', () => {
      const actions = [actionOne(), actionTwo()];
      actions$ = hot('-a-b', { a: actions[0], b: actions[1] });
    
      const response = cold('-a|', { a: data });
      userServiceSpy.getSomeDataViaHTTP.and.returnValue(response);
    
      const outcome = successAction({ payload: data });
      const expected = cold('--b-b', { b: outcome });
      expect(effects.someEffect$).toBeObservable(expected);
    });
    

    Marble testing is cool but it involves some black magic you should prepare for. I'd very much recommend you to carefully read this excellent article to have a deeper understanding of the subject.