Search code examples
angularngrxngrx-router-store

How to test ngrx router store selector


In our app we have a simple store, containing at root level an AuthState and a RouterState. The RouterState is created through @ngrx/router-store methods.

We have some selectors which have to use the RouterState to retrieve for example a param and then combine it for example with other selector result.

Our problem is we cannot manage to find a way to correctly setup test suite to be able to test such combined selectors.

Reducer setup

App module imports

StoreModule.forRoot(reducers, { metaReducers }),
StoreRouterConnectingModule.forRoot({
  stateKey: 'router',
}),
StoreDevtoolsModule.instrument(),

reducers beeing the following:

Reducers

export interface RouterStateUrl {
  url: string;
  queryParams: Params;
  params: Params;
}

export interface State {
  router: fromNgrxRouter.RouterReducerState<RouterStateUrl>;
  auth: fromAuth.AuthState;
}

export const reducers: ActionReducerMap<State> = {
  router: fromNgrxRouter.routerReducer,
  auth: fromAuth.reducer,
};

export const getRouterState = createFeatureSelector<fromNgrxRouter.RouterReducerState<RouterStateUrl>>('router');

export const getRouterStateUrl = createSelector(
  getRouterState,
  (routerState: fromNgrxRouter.RouterReducerState<RouterStateUrl>) => routerState.state
);

export const isSomeIdParamValid = createSelector(
  getRouterState,
  (routerS) => {
    return routerS.state.params && routerS.state.params.someId;
  }
);

Here is AuthState reducer:

export interface AuthState {
  loggedIn: boolean;
}

export const initialState: AuthState = {
  loggedIn: false,
};

export function reducer(
  state = initialState,
  action: Action
): AuthState {
  switch (action.type) {
    default: {
      return state;
    }
  }
}

export const getAuthState = createFeatureSelector<AuthState>('auth');
export const getIsLoggedIn = createSelector(
  getAuthState,
  (authState: AuthState) => {
    return authState.loggedIn;
  }
);

export const getMixedSelection = createSelector(
  isSomeIdParamValid,
  getIsLoggedIn,
  (paramValid, isLoggedIn) => paramValid && isLoggedIn
)

Test setup

@Component({
  template: ``
})
class ListMockComponent {}

describe('Router Selectors', () => {
  let store: Store<State>;
  let router: Router;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule.withRoutes([{
          path: 'list/:someId',
          component: ListMockComponent
        }]),
        StoreModule.forRoot({
          // How to add auth at that level
          router: combineReducers(reducers)
        }),
        StoreRouterConnectingModule.forRoot({
          stateKey: 'router',
        }),
      ],
      declarations: [ListMockComponent],
    });

    store = TestBed.get(Store);
    router = TestBed.get(Router);
  });

tests and their result

Test 1

it('should retrieve routerState', () => {
  router.navigateByUrl('/list/123');
  store.select(getRouterState).subscribe(routerState => console.log(routerState));
});

{ router: { state: { url: '/list/123', params: {someId: 123}, queryParams: {} }, navigationId: 1 }, auth: { loggedIn: false } }

as you can see the getRouterState selector doesn't retrieve only the router slice of the state but an object containing the whole routerState + authState State. router and auth are children of this object. So the selector doesn't manage to retrieve proper slices.

Test 2

it('should retrieve routerStateUrl', () => {
  router.navigateByUrl('/list/123');
  store.select(getRouterStateUrl).subscribe(value => console.log(value));
});

undefined - TypeError: Cannot read property 'state' of undefined

Test 3

it('should retrieve mixed selector results', () => {
  router.navigateByUrl('/list/123');
  store.select(getMixedSelection).subscribe(value => console.log(value));
});

undefined

TypeError: Cannot read property 'state' of undefined

TypeError: Cannot read property 'loggedIn' of {auth: {}, router: {}}

Note

Please note the syntax

StoreModule.forRoot({
  // How to add auth at that level
  router: combineReducers(reducers)
}),

seems mandatory if we want to combine selectors using multiple reducers. We could just use forRoot(reducers) but then we cannot test ONLY router selectors. Other parts of state would be inexistent.

For example, if we need to test:

export const getMixedSelection = createSelector(
  isSomeIdParamValid,
  getIsLoggedIn,
  (paramValid, isLoggedIn) => paramValid && isLoggedIn
)

we need both router and auth. And we cannot find a proper test setup which allows us to test such a combined selector using AuthState and RouterState.

The problem

How to setup this test so we can basically test our selectors?

When we run the app, it works perfectly. So the problem is only with testing setup.

We thought maybe it was a wrong idea to setup a testBed using real router. But we struggle to mock the routerSelector (only) and give it a mocked router state slice for testing purpose only.

It's really hard to mock these router selectors only. Spying on store.select is easy but spying on the store.select(routerSelectorMethod), with method as argument becomes a mess.


Solution

  • Now you can mock selector dependencies with projector property:

    my-reducer.ts

    export interface State {
      evenNums: number[];
      oddNums: number[];
    }
    
    export const selectSumEvenNums = createSelector(
      (state: State) => state.evenNums,
      (evenNums) => evenNums.reduce((prev, curr) => prev + curr)
    );
    export const selectSumOddNums = createSelector(
      (state: State) => state.oddNums,
      (oddNums) => oddNums.reduce((prev, curr) => prev + curr)
    );
    export const selectTotal = createSelector(
      selectSumEvenNums,
      selectSumOddNums,
      (evenSum, oddSum) => evenSum + oddSum
    );
    

    my-reducer.spec.ts

    import * as fromMyReducers from './my-reducers';
    
    describe('My Selectors', () => {
    
      it('should calc selectTotal', () => {
        expect(fromMyReducers.selectTotal.projector(2, 3)).toBe(5);
      });
    
    });
    

    Taken from official docs