如何使用 ReplaySubject() 对 ngrx/effects 中的 `mergeMap()` 函数进行单元测试?
How to unit-test `mergeMap()` function in ngrx/effects using ReplaySubject()?
我正在为 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 调度多个动作的效果测试?
这是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
- 不是一次。可能这就是测试失败的原因。
你的效果returns 两个动作new fromTodos.AddTodoItemSuccess(data), new fromTodos.LoadTodos()
。
在您的测试中,您使用 expect(result).toEqual(new fromActions.LoadTodos(), new fromActions.AddTodoItemSuccess(undefined));
,它检查返回的操作是否为 LoadTodos.
因为返回的第二个操作是成功操作,我们仍然验证操作是否为 LoadTodos
操作,导致错误。
终于明白了,感谢@timdeschryver 的提示。
您需要 take
和 skip
rxjs 运算符来测试第一个和第二个操作是否按预期调度。
- 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))
);
});
我正在为 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 调度多个动作的效果测试?
这是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
- 不是一次。可能这就是测试失败的原因。
你的效果returns 两个动作new fromTodos.AddTodoItemSuccess(data), new fromTodos.LoadTodos()
。
在您的测试中,您使用 expect(result).toEqual(new fromActions.LoadTodos(), new fromActions.AddTodoItemSuccess(undefined));
,它检查返回的操作是否为 LoadTodos.
因为返回的第二个操作是成功操作,我们仍然验证操作是否为 LoadTodos
操作,导致错误。
终于明白了,感谢@timdeschryver 的提示。
您需要 take
和 skip
rxjs 运算符来测试第一个和第二个操作是否按预期调度。
- 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))
);
});