You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为含服务调用的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 20:25:25