如何为含服务调用的NgRx Effects编写正确大理石测试?测试报错排查
NgRx Effect 与单元测试故障排查
问题描述
编写了如下NgRx Effect及对应的单元测试,但两个测试用例均在frame 0返回错误,无法定位是Effect本身还是测试代码的问题。
我的NgRx Effect代码
createListConfirm$ = createEffect(() => this.actions$.pipe( ofType(listActions.createListConfirm), withLatestFrom(this.store.select(projectSelectors.getCurrentProjectId)), switchMap(([action, projectId]) => this.listService.createList(action.payload, projectId)), switchMap(result => of(listActions.createListSuccess({ payload: result }))), catchError(() => of(listActions.createListFailed())), ), );
对应的单元测试代码
describe('createListConfirm$', () => { it('dispatches createListSuccess', () => { action = hot('a', { a: listActions.createListConfirm({ payload: createListReq }) }); const response = cold('a|', { a: payload }) const expected = cold('-b|', { b: listActions.createListSuccess({ payload }) }); expect(effects.createListConfirm$).toBeObservable(expected); }) it('dispatches createListFailed', () => { action = hot('a', { a: listActions.createListConfirm({ payload: createListReq }) }); const error = cold('#'); listService.createList.mockReturnValue(throwError(error)); const expected = cold('(b|', { b: listActions.createListFailed() }); expect(effects.createListConfirm$).toBeObservable(expected); }); });
测试配置代码
const createListReq: CreateListRequest<BaseListConfig> = { ...createListReq }; const payload: SubjectSummary = { ...payload }; const list: CreateListRequest<BaseListConfig> = { ...list }; let action: Observable<Action>; let effects: ListEffects; let mockStore: MockStore; let listService: jest.Mocked<ListService>; beforeEach( waitForAsync(() => { TestBed.configureTestingModule({ imports: [AngularTestingModule], providers: [ ListEffects, HttpClientTestingModule, BrowserAnimationsModule, Store, provideMockActions(() => action), provideMockStore({ initialState: { loaded: false, loading: false, listId: null, config: null, showFilterRow: false, features: null, }, }), { provide: ApiService, useValue: mockAndSpyOn(ApiService) }, { provide: ListService, useValue: { createList: jest.fn().mockReturnValue(cold('-a|',{ id: '123' })) }, }, ], }); effects = TestBed.inject(ListEffects); mockStore = TestBed.inject(MockStore); action = TestBed.inject(Actions); listService = TestBed.inject(ListService) as jest.Mocked<ListService>; }), );
问题分析与修复方案
1. Store选择器未正确Mock
Effect中使用projectSelectors.getCurrentProjectId从Store获取当前项目ID,但测试配置的Mock Store初始状态仅包含list相关切片,没有project切片,导致选择器无法获取值,直接抛出错误(这是frame 0报错的核心原因)。
修复:
在beforeEach中为该选择器设置Mock返回值:
beforeEach(() => { // ... 其他初始化代码 mockStore.overrideSelector(projectSelectors.getCurrentProjectId, 'mock-project-id'); });
2. 成功测试中未覆盖ListService的默认Mock
测试配置中listService.createList默认返回cold('-a|',{ id: '123' }),但成功测试期望返回自定义的payload,需要在测试用例中覆盖这个Mock。
修复成功测试:
it('dispatches createListSuccess', () => { action = hot('a', { a: listActions.createListConfirm({ payload: createListReq }) }); const response = cold('a|', { a: payload }); // 覆盖默认Mock,返回测试期望的响应 listService.createList.mockReturnValue(response); const expected = cold('-b|', { b: listActions.createListSuccess({ payload }) }); expect(effects.createListConfirm$).toBeObservable(expected); });
3. 错误测试中错误使用throwError
throwError接收的是错误值,而非Observable。你传入了cold('#')(一个错误Observable),应该直接将cold('#')作为createList的返回值。
修复错误测试:
it('dispatches createListFailed', () => { action = hot('a', { a: listActions.createListConfirm({ payload: createListReq }) }); const error$ = cold('#'); listService.createList.mockReturnValue(error$); const expected = cold('(b|)', { b: listActions.createListFailed() }); expect(effects.createListConfirm$).toBeObservable(expected); });
4. Effect中的catchError位置问题
当前Effect的catchError在整个管道最后,一旦发生错误,整个Effect会被终止(后续的createListConfirm动作不会被处理)。正确做法是将catchError放在switchMap内部,仅捕获单次请求的错误,不影响整个Effect的生命周期。
修复Effect:
createListConfirm$ = createEffect(() => this.actions$.pipe( ofType(listActions.createListConfirm), withLatestFrom(this.store.select(projectSelectors.getCurrentProjectId)), switchMap(([action, projectId]) => this.listService.createList(action.payload, projectId).pipe( switchMap(result => of(listActions.createListSuccess({ payload: result }))), catchError(() => of(listActions.createListFailed())) ) ), ), );
内容的提问来源于stack exchange,提问作者bitchwhocodes
相关产品推荐
相关产品推荐

