如何测试带有链式rxjs catchError的NgRx @Effect?
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

