Redux-Observable testing readonly epics

Viewed 139

I would like to test the following epic:

authEpics.logoutEpic = function (action$, _, deps) {
  return action$.pipe(
    ofType(authActions.logout),
    tap(() => {
      const { history, browser } = deps;
      browser.localStorage.removeItem('user');
      history.push(ROUTES.LOGIN);
    }),
    ignoreElements()
  );
};

Here's the test case:

describe('Logout Epic', () => {
  test('should remove user from localStorage', (done) => {
    scheduler.run((helpers) => {
      const { hot, expectObservable } = helpers;
      const action$ = hot('a', {
        a: authActions.logout(),
      });
      const deps = testUtils.mockEpicDependencies();
      const output$ = authEpics.logoutEpic(action$, null, deps);
      
      // Statement 1.
      expectObservable(output$).toBe('');

      // Statement 2.
      output$.subscribe({
        complete: () => {
          expect(deps.browser.localStorage.removeItem).toHaveBeenCalledWith('user');
          expect(deps.history.push).toHaveBeenCalledWith(ROUTES.LOGIN);
          done();
        }
      });
    });
});

Based on the definition of ignoreElements, I am not able grok the following observations:

  1. In Statement 1, I was of the opinion expectObservable(output$).toBe('|') should work, but it didn't. Instead expectObservable(output$).toBe('') works.
  2. In Statement 2, the complete function never gets called.
3 Answers

To make the test case work, I had to flush the promise queue (and not use expectObservable). Here's the revised test case:

describe('Logout Epic', () => {
  test('should remove user from localStorage', (done) => {
    scheduler.run(async (helpers) => {
      const { hot } = helpers;
      const action$ = hot('a', {
        a: authActions.logout(),
      });
      const deps = testUtils.mockEpicDependencies();
      const output$ = authEpics.logoutEpic(action$, null, deps);

      // *** Flush the promise queue ***
      await new Promise((resolve) => resolve());

      expect(deps.browser.localStorage.removeItem).toHaveBeenCalledWith('user');
      expect(deps.history.push).toHaveBeenCalledWith(ROUTES.LOGIN);

      done();
    });
});

This is so painful and does not work for for me.

const pingAuthEpic = (action$: any) => action$.pipe(
    ofType(SET_ONLINE),
    mergeMap(() => interval(PING_AUTH_INTERVAL).pipe(
        mergeMap(() => of({ type: 'PING_AUTH' })),
        takeUntil(action$.ofType(SET_OFFLINE)),
    )),
);

Any ideas why it just hangs and jest times out welcome...

Another option is to pass a flag in dependencies that tells the epic if it should emmit/ignoreElements or not, and default it to true. The test can then flag this as false and consume the output as normal.

If you want to use marble testing in order to validate timings (say you needed the calls to local storage to be async). You can use the rxjs testScheduler (https://github.com/ReactiveX/rxjs/blob/master/docs_app/content/guide/testing/marble-testing.md) and have the dependencies call a subject, which you can then pass to expectObservable. This allows you to check the dependencies are called a the right time with the right arguments.

Related