16

I am trying to test this epic https://github.com/zarcode/unsplashapp/blob/master/src/epics/photos.js . Problem is that map never happens when I run test (which I assume means that Promise never resolves), so photosSuccess action never happens also:

export const loadPhotosToList = (action$: Observable<Action>, state$: 
Object): Observable<Action> => {
  const state = (photosFilter: PhotosFilter) => state$.value.photos[photosFilter];
  return action$
  // .ofType(ACTION.FETCH_PHOTOS_REQUESTED)
    .pipe(
      filter((a: Action) =>
        a.type === ACTION.FETCH_PHOTOS_REQUESTED &&
        ((state(a.filter).loadingState === 'idle' && !state(a.filter).isLastPage) || a.refresh)),
      switchMap((a) => {
        const nextPage = !a.refresh ? state(a.filter).lastLoadedPage + 1 : 1;
        const loadingAction = of(photosActions.photosLoading(a.filter, a.refresh));
        const request = api.fetchPhotos({
          page: nextPage,
          per_page: perPage,
          order_by: a.filter,
        });
        const requestAction = from(request)
          .pipe(
            // tap(data => { console.log("data", data); }),
            map(data =>
              photosActions.photosSuccess(
                data,
                a.filter,
                nextPage,
                data.length < perPage,
                a.refresh,
              )),
            catchError(e => of(photosActions.photosFail(e.message, a.filter))),
          );
        // requestAction.subscribe(x => console.log("-------",x));
        return loadingAction
          .pipe(
            concat(requestAction),
            takeUntil(action$
              .pipe(filter(futureAction => futureAction.type === ACTION.FETCH_PHOTOS_REQUESTED))),
          );
      }),
    );
};

However, if I do requestAction.subscribe promise gets resolved and I get the result in the console log.

Note: this happens only when I run this test https://github.com/zarcode/unsplashapp/blob/master/src/epics/photos.test.js, app code works fine, data is fetching fine.

Question is: How to write this test properly?

Leo Jiang
  • 18,829
  • 38
  • 122
  • 215
zarcode
  • 2,301
  • 14
  • 29
  • When I run your test suite, I get `Invalid variable access: console` and all 6 fails, you know why? – Tarun Lalwani May 24 '18 at 10:25
  • Could you try replacing **return loadingAction** with **return Observable.fromPromise(loadingAction)**? – dentemm May 24 '18 at 17:34
  • @TarunLalwani maybe it has something to do with the node version, I am running it on v9.2.0 – zarcode May 25 '18 at 06:35
  • @dentemm `photosActions.photosLoading(a.filter, a.refresh)` is an object, not a Promise so doing return Observable.fromPromise(loadingAction) or from(loadingAction) with rxjs v6 throws an error of this kind: `TypeError: You provided an invalid object where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.` – zarcode May 25 '18 at 06:41
  • @zarcode, works now, I was on `10.1.0`, may be something has changed in that – Tarun Lalwani May 25 '18 at 07:50
  • @zarcode, bounty is about to expire. Please look at the existing answer – Tarun Lalwani May 30 '18 at 16:16

1 Answers1

3

Keep mind when testing async code you need to use different strategies, as noted in the documentation.

I crossed with a lot of problems before trying to test my async code, most of the problems were that the test was asserting the expected behavior before the resolved promise could be processed, and the assertion needed to be made in the next tick of the event loop, which I in practice achieve by putting the assertion inside a setImmediate, which can be achieved with a setTimeout too.

From the documentation of jest, you can change your test and pass the done callback, dispatch the triggering action as normal, but put the assertion inside a setTimeout callback with 1 ms timeout, just to allow the resolved promised to be processed, then the callback will be called and it will assert the desired state.

Other strategies can be used, like async/await, or you can instead of using setTimeout, resolve an empty Promise and put the assertion into the then callback. Just keep in mind that if more Promises exist within the tested flow, more ticks on the event loop will be necessary until the desired output is achieved to be asserted.

EDIT: Implementation:

// test
it('produces the photo model', (done) => {
...
// dispatch epic triggering action
store.dispatch(...);
// assertion
setImmediate(() => {
  expect(store.getActions()).toEqual([
  ...
  ]);
  done();
});
rodgobbi
  • 1,272
  • 13
  • 10
  • You have the repo in the question, instead of giving a theoretical answer, it would be great if you could just fix the test and update the same in your answer. I tried your recommendation and not able to fix it. So its best if you just check it once – Tarun Lalwani May 27 '18 at 05:34
  • I don't agree with "instead", because there's no point for me in fixing the problem without explaining why the problem is happening and how to fix it, this is the best for everyone's learning, including to me. But no problem in helping more if the problem persists, it's actually faster, and I posted the solution in this [pull request](https://github.com/zarcode/unsplashapp/pull/1). – rodgobbi May 27 '18 at 16:25
  • And as exactly as I've written in the answer, pass the `done` callback which `jest` exposes, assert inside the `setImmediate` callback and call `done` inside it too, I'll update the answer to show the implementation to be more visual. – rodgobbi May 27 '18 at 16:31