Raj
Raj

Reputation: 1120

How to unit-test `mergeMap()` function in ngrx/effects using ReplaySubject()?

I am writing unit-test for Angular ngrx/effects, for a todo app. I am using ReplaySubject() as they are more intuitive and easy to test, instead of jasmine marbels (hot and cold).

But i am getting following error.

    should dispatch success and error actions for AddTodoItem
        HeadlessChrome 74.0.3729 (Linux 0.0.0)
        Error: Expected object to be a kind of AddTodoItemSuccess, but was LoadTodos({ type: '[Todo] Load Todos' }).
            at <Jasmine>
            at SafeSubscriber._next (src/app/store/effects/app.effects.spec.ts:46:24)
            at SafeSubscriber.__tryOrUnsub (node_modules/rxjs/_esm2015/internal/Subscriber.js:183:1)
            at SafeSubscriber.next (node_modules/rxjs/_esm2015/internal/Subscriber.js:122:1)

App.effects.ts

@Effect()
  loadTodos$ = this.actions$.pipe(
    ofType<fromTodos.LoadTodos>(TodoActionTypes.LOAD_TODOS),
    switchMap((action) => {
      return this.todoService.getTodos().pipe(
        map(data => {
          return new fromTodos.LoadTodosSuccess(data);
        }),
        catchError(err => of(new fromTodos.LoadTodosFailure(err)))
      );
    })
  );

  @Effect()
  addTodo$: Observable<Action> = this.actions$.pipe(
    ofType<fromTodos.AddTodoItem>(TodoActionTypes.ADD_TODO_ITEM),
    switchMap(action => {
      return this.todoService.addTodo(action.payload).pipe(
        mergeMap(data => {
          return [new fromTodos.AddTodoItemSuccess(data),  new fromTodos.LoadTodos()];
        }),
        catchError(err => of(new fromTodos.AddTodoItemFailure(err)))
      );
    })
  );

app.effects.spec.ts

describe('AppEffects', () => {
  let actions$: ReplaySubject<any>;
  let effects: AppEffects;
  const testTodo: Todo = {
    id: 0,
    todo: 'string',
    mark_as_done: true,
  };

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        AppEffects,
        provideMockActions(() => actions$)
      ]
    });

    effects = TestBed.get(AppEffects);
  });

  // passsing
  it('should dispatch success and error actions for LoadTodos', () => {
    actions$ = new ReplaySubject(1);
    actions$.next(new fromActions.LoadTodos());

    effects.loadTodos$.subscribe(
      result => expect(result).toEqual(new fromActions.LoadTodosSuccess(null), 'should dispatch'),
      err => expect(err).toEqual(new fromActions.LoadTodosFailure(null))
    );
  });

  // failing
  it('should dispatch success and error actions for AddTodoItem', () => {
    actions$ = new ReplaySubject(1);
    actions$.next(new fromActions.AddTodoItem(testTodo));

    effects.addTodo$.subscribe(
      result => { console.log('AddTodoItem', result);
        expect(result).toEqual(new fromActions.AddTodoItemSuccess(undefined), new fromActions.LoadTodos());
      },
      err => expect(err).toEqual(new fromActions.AddTodoItemFailure(err))
    );
  });
});

Screenshot of error

error screenshot


I referred to ngrx documentation, but it did not have any example of mergeMap. How to write the test for effects where multiple actions are dispatched using mergeMap?

Upvotes: 0

Views: 4026

Answers (3)

Raj
Raj

Reputation: 1120

Finally figured out, thanks @timdeschryver for hints.

You need take and skip operators of rxjs to test if the first and second actions are dispatched as expected.

  • take(2) takes only first 2 events of the observable
  • skip(2) skips the first 2 events of observable

app.effects.ts

  @Effect()
  addTodo$: Observable<Action> = this.actions$.pipe(
    ofType<fromTodos.AddTodoItem>(TodoActionTypes.ADD_TODO_ITEM),
    switchMap(action => {
      return this.todoService.addTodo(action.payload).pipe(
        mergeMap(data => {
          return [new fromTodos.AddTodoItemSuccess(data), new fromTodos.LoadTodos()];
        }),
        catchError(err => of(new fromTodos.AddTodoItemFailure(err)))
      );
    })
  );

app.effets.spec.ts

  it('should dispatch success and error actions for AddTodoItem', () => {
    actions$ = new ReplaySubject(1);
    actions$.next(new fromActions.AddTodoItem(testTodo));

    effects.addTodo$
          .pipe(take(1))  // this takes only the first event of the observable
          .subscribe(
            result => expect(result).toEqual(new fromActions.AddTodoItemSuccess(undefined), 'first action should be AddTodoItemSuccess'),
            err => expect(err).toEqual(new fromActions.AddTodoItemFailure(err))
          );

    effects.addTodo$
          .pipe(skip(1))  // this skips the first event of observable, and takes from second event (i.e. the second action alone will be available now)
          .subscribe(
            result => expect(result).toEqual(new fromActions.LoadTodos(), 'second action should be LoadTodos'),
            err => expect(err).toEqual(new fromActions.AddTodoItemFailure(err))
          );
  });

Upvotes: 4

Buczkowski
Buczkowski

Reputation: 2416

This is order of returned value in loadTodo$ effect:

[new fromTodos.AddTodoItemSuccess(data), new fromTodos.LoadTodos()]

and this is your assertion:

expect(result).toEqual(new fromActions.LoadTodos(), new fromActions.AddTodoItemSuccess(undefined));

even error tolds you about that:

Expected object to be kind of LoadTodos, but was AddTodoItemSuccess...

Edit1

Second argument in toEqual method is expectationFailOutput which is short info that you would like to output when assertion fails. Note that in subscribe body you firstly get AddTodoItemSuccess and then LoadTodos - not at once. Probably this is reason why test fails.

Upvotes: 0

timdeschryver
timdeschryver

Reputation: 15505

Your effect returns two actions new fromTodos.AddTodoItemSuccess(data), new fromTodos.LoadTodos().

In your test you use expect(result).toEqual(new fromActions.LoadTodos(), new fromActions.AddTodoItemSuccess(undefined));, which checks if the action returned is LoadTodos. Because the second action returned is the success action, we still verify if the action is the LoadTodos action, resulting in the error.

Upvotes: 1

Related Questions