6

I have been trying to get this working for hours. This is the first project I have converted over to Jest instead of Karma and so far so good. I went to write some tests for my effects though and for whatever reason, I am completely unable to test them in the way I would expect.

The effect I am trying to test is a pretty simple navigate one:

@Effect({ dispatch: false })
go$ = this.actions$.pipe(
  ofType(RouterActions.GO),
  tap(
    ({ payload: { path, query: queryParams, extras } }: RouterActions.Go) => {
      this.router.navigate(path, { queryParams, ...extras });
    }
  )
);

I have injected a fake router and was going to test that it was calling navigate, that test has gone through many iterations trying to get it to work but the one I currently have is:

describe('Router Effects', () => {
  let actions$: Observable<any>;
  let router: TestRouter;
  let effects: RouterEffects;
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        RouterEffects,
        provideMockActions(() => actions$),
        {
          provide: Router,
          useFactory: getRouter
        }
      ]
    });

    actions$ = TestBed.get(Actions);
    router = TestBed.get(Router);
    effects = TestBed.get(RouterEffects);
  });

  describe('go$', () => {
    test('should call router.navigate with the correct path', done => {
      const action = new fromActions.Go({ path: ['some', 'path'] });

      actions$ = hot('-a', { a: action });
      const expected = cold('-b', { b: action });

      effects.go$.subscribe(
        result => {
          console.log('inside subscribe?');
          expect(router.navigate).toHaveBeenCalled();
          console.log('after expect');
          done();
          console.log('after done?');
        },
        done,
        done
      );

      expect(effects.go$).toBeObservable(expected);
    });
  });
});

When I run that test, I get the following results in my terminal:

FAIL  src/app/store/effects/router.effects.spec.ts (5.832s)
  ● Console

    console.log src/app/store/effects/router.effects.spec.ts:48
      inside subscribe?
    console.log src/app/store/effects/router.effects.spec.ts:50
      after expect
    console.log src/app/store/effects/router.effects.spec.ts:52
      after done?

  ● Router Effects › go$ › should call router.navigate with the correct path

    Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.

I have been pulling my hair out trying to figure out why that async callback is not being called? I have reproduced a minimal repo here: https://github.com/BenAychh/example-ngrx-effects-jest and the code in question is located in this folder https://github.com/BenAychh/example-ngrx-effects-jest/tree/master/src/app/store/effects. If anyone is interested in helping me out here, you should be able to clone down that entire repo and just run it and (hopefully) see what I am seeing.

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Ben Hernandez
  • 857
  • 1
  • 11
  • 23

2 Answers2

4

Given this effect:

@Injectable()
export class AuthEffects {
    @Effect({ dispatch: false })
    public logoutSuccess$ = this.actions$.pipe(
        ofType(AuthActionTypes.LogoutSuccess),
        tap(() => this.localStorage.removeItem(AUTH_FEATURE_KEY)),
        tap(() => this.router.navigate(['/']))
    );
}

This is the way that works for me to test a dispatch: false effect:

describe('logoutSuccess$', () => {
    it('should navigate and remove related data from local storage', () => {
        const action = new LogoutSuccess;
        actions$ = cold('-a', { a: action });

        expect(effects.logoutSuccess$).toBeObservable(actions$);
        expect(localStorageService.removeItem).toHaveBeenCalledWith(AUTH_FEATURE_KEY);
        expect(router.navigate).toHaveBeenCalledWith(['/']);
    });
});

The .toBeObservable(actions$) do the trick.

Based on this answer.

J0H4N_AC
  • 499
  • 1
  • 5
  • 10
  • Yeah, that is usually how I do it too, but I couldn't get it to work with jest-marbles. Works great with jasmine-marbles though. – Ben Hernandez Jan 28 '19 at 16:22
0

Stumbled upon this an hour earlier and after searching for a while, found an issue in jest-marbles that led me to finding

toSatisfyOnFlush

You'd use this as

const expected = hot('-');

expect(effect.method$).toBeObservable(expected);

expect(expected).toSatisfyOnFlush(() => {
  // do your verifications here.
  expect(router.navigate).toHaveBeenCalledWith([]);
});

Of course, you want to ensure your expected matches how your action ends. I had another action that ended with

switchMap(() => this.router.navigate([]))

before { dispatch: false }

This would cause the final Observable to be of

hot('-a', { a: true }); 

since the switchMap would cause the Observable stream to have the latest value 'true' that was returned from router.navigate().

I hope this helps future viewers.

Anjil Dhamala
  • 1,544
  • 3
  • 18
  • 37