3

I'm struggling on how to test the retryWhen operator in a redux-observable epic. Based on this example taken from docs, I forked this jsbin where I'm trying to test the case where the response fails 2 times and after that it returns a valid response.

Below are some part of the code. For the whole implementation please use this jsbin

let RetryStrategy = attempts => attempts
    .zip(Observable.range(1, 4))
    .flatMap(([error, i]) => {
        if (i > 3) {
            return Observable.throw('Network error occured')
        }
        return Observable.timer(i * 1000)
    })


const fetchFooEpic = (action$, store, call = indirect.call) =>
    action$.ofType('FETCH_FOO')
        .mergeMap(action =>
            call(api.fetchFoo, action.payload.id)
                .map(payload => ({ type: 'FETCH_FOO_FULFILLED', payload }))
                .retryWhen(RetryStrategy)
                .takeUntil(action$.ofType('FETCH_FOO_CANCELLED'))
                .catch(error => of({
                    type: 'FETCH_FOO_REJECTED',
                    payload: error.xhr.response,
                    error: true
                }))
        );

describe('fetchFooEpic', () => {
    ...
    it.only('handles errors correctly', () => {
        const badResponse = { message: 'BAD STUFF' };
        const response = { id: 123, name: 'Bilbo' };

        expectEpic(fetchFooEpic, {
            expected: ['-----a|', {
                a: { type: 'FETCH_FOO_FULFILLED', payload: response }
            }],
            action: ['(a|)', {
                a: { type: 'FETCH_FOO', payload: { id: 123 } }
            }],
            response: ['-#-#-a|', {
                a: response
            }, { xhr: { badResponse } }],
            callArgs: [api.fetchFoo, 123]
        });
    });
    ...

});

If you check the response in jsbin the actual action in always an empty array.

stelioschar
  • 115
  • 1
  • 2
  • 10
  • Hello! Have you tried testing your retryWhen without everything else around it and all the abstractions? It might be tough for someone to help you as I or they would need to spend a non-trivial amount of time to understand everything. – jayphelps Aug 07 '17 at 19:55
  • @jayphelps thank you for your quick reply. The problem seems to be that when using `retry` or `retryWhen` retries to do the whole action from the beginning and not just retry the inner ajax observable(inside switchMap). I made a lot of tests based on the above example and as you can see [here](http://jsbin.com/hekener/36/edit?js,output) if you try to change the number of the retries(`.retry(3)`) or change the response marble (`response: ['--#a|'`) you can see that the actual frame is a multiplication of the frames and the number of the retries. – stelioschar Aug 09 '17 at 08:39

1 Answers1

2

I had a similar problem where I was trying to test an Angular HttpInterceptor that tries up to three times with a delay between tries. As you mentioned in the comments, the retryWhen resubscribes to the observable after each error. This means that if you have an error observable (e.g. cold('#|')), you will always get an error in the retryWhen because is resubscribes to the same error observable on each retry.

It seems like a hack, but I created this simple class that subscribes to different observables in the order given.

  class MultiObservable extends Observable<any> {
    constructor(observables: Observable<any>[]) {
      let subscriptionIdx = 0;
      super((subscriber: Subscriber<any>) => 
               observables[subscriptionIdx++].subscribe(subscriber));
    }
  }

In my test I use it as follows:

  const testObservable = new MultiObservable([
    cold('#|', null, { status: 400 }),
    cold('a|')
  ]);

  next.handle.and.returnValue(testObservable);
  const actual = interceptor.intercept(req, next);
  expect(actual).toBeObservable(cold('---a|'));

I'm hoping someone else will have a less hacky solution but this is working for me now.

Robert Mitchell
  • 133
  • 1
  • 6