How can I unit test a retryWhen operator in rxjs?
Asked Answered
G

3

5

I am attempting to unit test a custom RxJS operator. The operator is very simple, it uses RetryWhen to retry a failed HTTP request, but has a delay and will only retry when the HTTP Error is in the 500 range. Using jasmine, and this is in an Angular application.

I've looked at this:

rxjs unit test with retryWhen

Unfortunately, updating the SpyOn call doesn't seem to change the returned observable on successive retries. Each time it retries it is retrying with the original spyon Value.

I have also looked at a bunch of rxjs marble examples, none of which seem to work. I am not sure it is possible to use rxjs marbles here, because (AFAIK) there is no way to simulate a situation where you first submit an errored observable, then submit a successful observable on subsequent tries.

The code is basically a clone of this: https://blog.angularindepth.com/retry-failed-http-requests-in-angular-f5959d486294

export function delayedRetry(delayMS: number, maxRetry) {
    let retries = maxRetry;

    return (src: Observable<any>) =>
        src.pipe(
            retryWhen((errors: Observable<any>) => errors.pipe(
                delay(delayMS),
                mergeMap(error =>
                    (retries-- > 0 && error.status >= 500) ? of(error) : throwError(error))

            ))
        );
}

I would like to be able to demonstrate that it can subscribe to an observable that returns an error on the first attempt, but then returns a successful response. The end subscription should show whatever success value the observable emits.

Thank you in advance for any insights.

Gillett answered 8/8, 2019 at 15:10 Comment(0)
M
7

Try to use this observable as source observable for your test:

const source = (called,successAt)=>{
    return defer(()=>{
        if(called<successAt){
            called++
            return throwError({status:500})
        }
        else return of(true)
    })
}

Test:

this.delayedRetry(1000,3)(source(0,3)).subscribe()
Misdemeanor answered 9/8, 2019 at 7:59 Comment(1)
that helped me out, I didn't know you could do that with the source, cool.Gretchengrete
J
3

To test the retry functionality, you need a observable which emits different events each time you call it. For example:

let alreadyCalled = false;
const spy = spyOn<any>(TestBed.inject(MyService), 'getObservable').and.returnValue(
  new Observable((observer) => {
    if (alreadyCalled) {
      observer.next(message);
    }
    alreadyCalled = true;
    observer.error('error message');
  })
);

This observable will emit an error first and after that a next event.

You can check, if your observable got the message like this:

it('should retry on error', async(done) => {
    let alreadyCalled = false;
    const spy = spyOn<any>(TestBed.inject(MyDependencyService), 'getObservable').and.returnValue(
      new Observable((observer) => {
        if (alreadyCalled) {
          observer.next(message);
        }
        alreadyCalled = true;
        observer.error('error message');
      })
    );
    const observer = {
      next: (result) => {
        expect(result.value).toBe(expectedResult);
        done();
      }
    }

    subscription = service.methodUnderTest(observer);
    expect(spy).toHaveBeenCalledTimes(1);
}
Jemadar answered 5/3, 2021 at 9:51 Comment(0)
S
0

Building on a previous answer I have been using this, which gives you more control over what's returned.

const source = (observables) => {
  let count = 0;
  return defer(() => {
    return observables[count++];
  });
};

Which can then be used like this

const obsA = source([
  throwError({status: 500}),
  of(1),
]);

Or it can then be used with rxjs marbles like

const obsA = source([
  cold('--#', null, { status: 500 }),
  cold('--(a|)', { a: 1 }),
]);
Shayla answered 6/1, 2021 at 21:38 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.