Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a mock observable to test http rxjs retry/retryWhen in Angular?

I have a http request of this form in my angular app: this.http.get(url,options).pipe(retry(5))

I want to unit test this using jasmine, such that the http request first returns an error but on a subsequent try, returns the expected data.

I previously returned http errors in this format as recommended by the angular.io documentation spyOn(http,'get').and.returnValue(defer(() => Promise.reject(errorObject)));

The rxjs operator retry does not seem to call the http.get function again, so changing the return value of the spy does not work. What I think I need to do is somehow return an observable from the spy which first emits the error and later emits the data. I thought about using a BehaviorSubject but don't think that accepts errors to be passed.

Is there any way of achieving this?

like image 729
aks94 Avatar asked Nov 08 '22 04:11

aks94


1 Answers

If we go to angular source code we will see next:

// Start with an Observable.of() the initial request, and run the handler (which
// includes all interceptors) inside a concatMap(). This way, the handler runs
// inside an Observable chain, which causes interceptors to be re-run on every
// subscription (this also makes retries re-run the handler, including interceptors).
const events$: Observable<HttpEvent<any>> =
    of (req).pipe(concatMap((req: HttpRequest<any>) => this.handler.handle(req)));

To make request retriable they are starting it with of(req), so we could do the same to emulate this behavior in unit tests. For example:

spyOn(http,"get").and.returnValue(
    createRetriableStream(
       throwError("err"),
       throwError("err"),
       throwError("err"),
       of("a")
    )
);

Where createRetriableStream function could look like it:

function createRetriableStream(...resp$: any): any {
   let fetchData: Spy = createSpy("fetchData");
   fetchData.and.returnValues(...resp$);
   return of(undefined).pipe(switchMap((_) => fetchData()));
}

Usually, retry logic is used together with a delay between attempt, to cover this part you could use time progression syntax. So your tests could look like:

    it("throw error after 2 retries", () => {
        testScheduler.run(({ cold, expectObservable }) => {
            source$ = createRetriableStream(cold("-#"), cold("-#"), cold("-#"),cold("-3")).pipe(
                retryWhen(
                   return errors.pipe(
                       mergeMap((err) => {                
                           return 2 > repeatCount
                               ? throwError(err)
                               : timer(100, testScheduler);             
                       })
                  );)
             );
        expectObservable(source$).toBe("- 200ms --#");
  });
like image 156
ilyabasiuk Avatar answered Nov 14 '22 21:11

ilyabasiuk