Effect stops listening after service error in NgRx Effects integration test
0 reputation · 25 Mar 2024, 00:58 UTC
0 reputation · 25 Mar 2024, 00:58 UTC
Goal is to validate an NgRx Effect integration without importing production credentials, environment files, or real network endpoints. The effect is the boundary between intent actions and a gateway service, and tests should assert the observable contract of success and failure actions using a mocked Actions stream and an overridden API configuration token.
Constraints are to keep the test free of real base URLs and tokens by overriding the injection token that provides API configuration, and to use provideMockActions with a fake backend for the service call. The documented failure condition is that an effect can stop listening or emit no failure action when error handling is misplaced in the operator chain.
Retry policy for transient failures remains an unresolved design decision for the team. The test needs to encode the chosen behavior explicitly without relying on production wiring.
Which observable shape should a fake gateway return to simulate a service error while keeping the outer actions stream alive? Should the test assert emission of a typed failure action and continued subscription for a subsequent dispatch? Is retry intended to be implemented inside the effect operator chain or delegated to a caller?
35275 reputation · 25 Mar 2024, 08:36 UTC
To simulate a service error without killing the outer actions stream, the fake gateway must return an observable that emits an error—typically via throwError(() => new Error('...'))—and the Effect must handle this error inside the flattening operator (e.g., switchMap, mergeMap, or exhaustMap).
The fake gateway should return a cold observable that terminates with an error notification. In a mock service, this is implemented as:
// Fake Gateway Implementation
getDetails(id: string) {
return throwError(() => new Error('API_FAILURE'));
}
The critical requirement is the placement of the catchError operator. If catchError is placed on the main actions$.pipe(...) chain, the error propagates to the root stream, completing it and stopping the Effect from listening to any further actions.
The correct pattern isolates the failure to the inner observable:
this.actions$.pipe(
ofType(ApiActions.loadData),
switchMap(action =>
this.gatewayService.call().pipe(
map(data => ApiActions.loadSuccess({ data })),
// Catch error HERE to keep the outer stream alive
catchError(error => of(ApiActions.loadFailure({ error: error.message })))
)
)
)
The test should assert both the immediate failure and the continued viability of the stream. This prevents "silent" stream deaths that only appear in production.
loadFailure action is emitted.loadSuccess action is emitted.Retry logic should be implemented inside the inner operator chain, preceding the catchError. This ensures that transient failures are handled before the Effect gives up and dispatches a failure action to the store. Delegating retry to a caller is generally avoided in NgRx, as the Effect is intended to encapsulate the side-effect orchestration.
This solution assumes the use of NgRx 12+ and RxJS 6/7. If the Effect still stops listening despite the inner catchError, please provide the full operator chain of the Effect, as an misplaced filter or takeUntil may be causing premature completion.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.