【问题标题】:How to unit-test `mergeMap()` function in ngrx/effects using ReplaySubject()?如何使用 ReplaySubject() 在 ngrx/effects 中对 `mergeMap()` 函数进行单元测试?
【发布时间】:2019-10-18 17:46:35
【问题描述】:

我正在为一个待办事项应用程序编写 Angular ngrx/effects 的单元测试。 我正在使用 ReplaySubject(),因为它们更直观且易于测试,而不是 jasmine marbels(热和冷)。

但我收到以下错误。

    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))
    );
  });
});

错误截图


我参考了ngrx 文档,但它没有任何mergeMap 的示例。 如何为使用 mergeMap 调度多个操作的效果编写测试

【问题讨论】:

  • 我已经完成了console.log() 的结果,看看发生了什么,我可以看到两个动作是在不同的时间分派的。 screenshot

标签: angular unit-testing ngrx-effects


【解决方案1】:

你的效果返回两个动作new fromTodos.AddTodoItemSuccess(data), new fromTodos.LoadTodos()

在您的测试中,您使用expect(result).toEqual(new fromActions.LoadTodos(), new fromActions.AddTodoItemSuccess(undefined));,它检查返回的操作是否为LoadTodos. 因为返回的第二个操作是成功操作,所以我们仍然验证该操作是否是LoadTodos 操作,从而导致错误。

【讨论】:

  • 嗨,蒂姆,感谢您检查这个问题。您的建议是颠倒规范文件AddTodoItemSuccess 然后LoadTodos 中的操作顺序。我已经试过了,没有用。 screenshot
  • 您是否可以建议expect 语句的正确语法?测试那个mergeMap??
  • 不颠倒顺序,但您需要检查调度的第一个动作是否是成功动作,第二个动作是否是加载动作。
  • 好的,我明白了,谢谢你的提示。我刚刚了解了takeskip rxjs 运算符。这对我有用。
【解决方案2】:

终于想通了,感谢@timdeschryver 的提示。

您需要 rxjs 的 takeskip 运算符来测试第一个和第二个操作是否按预期分派。

  • take(2) 只获取 observable 的前 2 个事件
  • skip(2) 跳过 observable 的前 2 个事件

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))
          );
  });

【讨论】:

    【解决方案3】:

    这是loadTodo$效果中返回值的顺序:

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

    这是你的断言:

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

    甚至错误告诉你:

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

    编辑1

    toEqual 方法中的第二个参数是expectationFailOutput,这是您希望在断言失败时输出的简短信息。请注意,在subscribe 正文中,您首先获得AddTodoItemSuccess,然后是LoadTodos - 不是一次。可能这就是测试失败的原因。

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-05-15
    • 2019-06-23
    • 1970-01-01
    • 2018-04-18
    • 2021-08-21
    • 2021-01-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多