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

如何测试带有链式rxjs catchError的NgRx @Effect?

Testing Chained catchError in NgRx Effects: Fix for Unreachable Operators

I ran into a frustrating issue while writing Jasmine tests for an NgRx Effect that used chained RxJS catchError operators—my tests never hit the second catchError, and I couldn't get the expected actions to emit. Here's what went wrong and how I fixed it:

The Original Effect

My initial Effect code tried to chain two catchError operators to emit different actions on failure:

@Effect() submitEndsheets$ = this.actions$.pipe(
    ofType<SubmitEndSheets>(SpreadActionTypes.SUBMIT_ENDSHEETS),
    withLatestFrom(this.store.pipe(select(fromAppStore.fromOrder.getDocumentId))),
    concatMap(([action, documentId]) => this.spreadService.submitEndSheets(documentId).pipe(
        map((response: ActionProcessorDto) => new SubmitEndSheetsSuccess(response.data)),
        catchError((error) => of(undo(action))),
        catchError((error) => of(new MessageModal({ message: error.message, title: 'Submission Error!' }) ))
    ) )
);

My Failing Test

I wrote a test expecting both error actions to emit, but only the first one showed up:

it('handles errors by sending an undo action', () => {
    const action = { type: SpreadActionTypes.SUBMIT_ENDSHEETS, };
    const source = cold('a', { a: action });
    const error = new Error('Error occurred!');
    const service = createServiceStub(error);
    const store = createStoreState();
    const effects = new Effects(service, new Actions(source), store);
    const expected = cold('ab', {
        a: undo(action),
        b: new MessageModal({ message: 'Sorry, something went wrong with your request. Please try again or contact support.', title: 'Update Error!' }),
    });
    expect(effects.submitEndsheets$).toBeObservable(expected);
});

Helper Mock Functions

Here are the stubs I used for the service and store:

function createServiceStub(response: any) {
    const service = jasmine.createSpyObj('spreadService', [
        'load', 'update', 'updateSpreadPosition', 'submitEndSheets'
    ]);
    const isError = response instanceof Error;
    const serviceResponse = isError ? throwError(response) : of(response);
    service.load.and.returnValue(serviceResponse);
    service.update.and.returnValue(serviceResponse);
    service.updateSpreadPosition.and.returnValue(serviceResponse);
    service.submitEndSheets.and.returnValue(serviceResponse);
    return service;
}

function createStoreState() {
    const store = jasmine.createSpyObj('store', ['pipe']);
    store.pipe.and.returnValue(of({ documentId: 123 }));
    return store;
}

Test Failure Output

The test failed because only one action was emitted instead of two:

FAILED TESTS:
✖ handles errors by sending an undo action
HeadlessChrome 0.0.0 (Mac OS X 10.14.2)
Expected $.length = 1 to equal 2.
Expected $[1] = undefined to equal Object({ frame: 10, notification: Notification({ kind: 'N', value: MessageModal({ payload: Object({ message: 'Sorry, something went wrong with your request. Please try again or contact support.', title: 'Update Error!' }), type: 'MESSAGE_MODAL' }), error: undefined, hasValue: true }) }).
at compare node_modules/jasmine-marbles/bundles/jasmine-marbles.umd.js:389:1)
at UserContext.<anonymous> src/app/book/store/spread/spread.effects.spec.ts:197:46)
at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke node_modules/zone.js/dist/zone.js:388:1)

The Fix: Return an Array of Actions from a Single catchError

I realized the problem with chained catchError operators: once the first catchError handles an error and emits a new Observable (of(undo(action))), the second catchError only listens for errors from that new Observable—which never emits an error.

Instead, I changed the catchError to return an array of actions directly. RxJS automatically converts this array into a stream of emitted actions, so both actions are sent out when an error occurs.

Corrected Effect Code

@Effect() submitEndsheets$ = this.actions$.pipe(
    ofType<SubmitEndSheets>(SpreadActionTypes.SUBMIT_ENDSHEETS),
    withLatestFrom(this.store.pipe(select(fromAppStore.fromOrder.getDocumentId))),
    concatMap(([action, documentId]) => this.spreadService.submitEndSheets(documentId).pipe(
        map((response: ActionProcessorDto) => new SubmitEndSheetsSuccess(response.data)),
        catchError(error => [
            new PopSingleToast({ severity: ToastSeverity.error, summary: 'Failure', detail: `Some error occurred: \n Error: ${error}` }),
            undo(action)
        ])
    ) )
);

Corrected Test Code

To test this correctly, I used parentheses in the marble syntax '(ab)' to indicate that both actions are emitted synchronously in the same frame:

it('handles errors by sending an undo action', () => {
    const action = { type: SpreadActionTypes.SUBMIT_ENDSHEETS };
    const source = cold('a', { a: action });
    const error = new Error('Error occurred!');
    const service = createServiceStub(error);
    const store = createStoreState();
    const effects = new Effects(service, new Actions(source), store);
    const expectedAction = new PopSingleToast({ severity: ToastSeverity.error, summary: 'Failure', detail: `Some error occurred: \n Error: ${error}` });
    const expected = cold('(ab)', { a: expectedAction, b: undo(action) });
    expect(effects.submitEndsheets$).toBeObservable(expected);
});

This fixed the test and ensured both error actions are emitted as expected!

内容的提问来源于stack exchange,提问作者mpaarating

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.11 09:07:50