angularngrxmarble-diagram

ngrx - marble testing and withLatestFrom


I'm using Angular 8 and ngrx 8. I'm trying to write unit tests for an effect that uses withLatestFrom to get some values from the state. I'm using rxjs/testing/TestScheduler for this. In there, I have a marble diagram like this:

actions$ = hot('aa', { a: actions.fetchUser({ id: 1 }));

and my effect is similar to this:

fetchUser$ = createEffect(() => this.actions$.pipe(
  ofType(actions.fetchUser),
  withLatestFrom(this.store.select(selectors.user)),
  mergeMap(([{ id }, user]) => {
    console.log(user);
    if (user.id === id) {
      return of(user);
    }
    return this.userService.getUser(id).pipe(
      map((user) => actions.updateUser({ user })),
      catchError(() => of(actions.updateUser({})))
    )
  })
))

The inial user in the store is an empty object.

The idea is the first marble frame goes through by calling userService and updates the state; then the second frame happens and it sees the user.id, which is set during the first frame, and so instead of calling userService, it returns the user instance that is already in the state. (This is just an example; the final goal is to avoid duplicate HTTP calls in the service or cancel the previous one if the user id changes).

The issue is that it seems the state doesn't update during the marble diagram and the user object returned from withLatestFrom is always the one from the initial state set in the tests.

I'm new to Angular and ngrx testing, so I'm not sure if that's the expected behavior or I'm doing something wrong.

It'd also be great if anyone can recommend a better way of handling and testing such a scenario.


Solution

  • I saw this note in the documentation for the mock store that says "All dispatched actions don't affect the state", so I guess the state doesn't change during a marble diagram.

    I changed my test so it sets the state with a user before getting into the marble stuff. It's now something like this:

    it('should use the user in store when called with the same id', () => {
      scheduler.run(({ hot, expectObservable }) => {
        const fetchUser = actions.fetchUser({ id: 1 });
        const updateUser = actions.updateUser({ user });
    
        store.setState({
          user
        });
    
        spyOn(userService, 'getUser').and.callThrough();
    
        actions$ = hot(
          `a`,
          { a: fetchUser }
        );
    
        // The result should come immediately because the user with the given id is already in the store
        const expectedMarble = `a`;
        const expectedValues = { a: updateUser };
    
        expectObservable(effects.fetchUser$).toBe(expectedMarble, expectedValues);
        });
    
      expect(userService.getUser).not.toHaveBeenCalledWith(1);
    });