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.