1

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 3

1

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();
    });
});
Sign up to request clarification or add additional context in comments.

Comments

0

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...

2 Comments

Can you try changing takeUntil like this: takeUntil(action$.pipe(ofType(SET_OFFLINE))) If it doesn't work, can you paste the test case code here?
Hey thanks Anup - I decided instead to test the whole store and pass in just the reducers and epics I want to test... It's much better because I don't need to understand marbles or whatever is wrong with the way I'm using the testing library. Also the test will still work if we move away from the unmaintained redux-observable.
0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.