首页 文章

如何测试ngrx路由器存储选择器

提问于
浏览
2

在我们的应用程序中,我们有一个简单的商店,在根级别包含 AuthStateRouterState . RouterState 是通过 @ngrx/router-store 方法创建的 .

我们有一些选择器必须使用RouterState来检索例如一个参数,然后将它与例如其他选择器结果组合 .

我们的问题是我们无法设法找到正确设置测试套件的方法,以便能够测试这样的组合选择器 .

减速机设置

App模块导入

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

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;
  }
);

这是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
)

测试设置

@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);
  });

测试及其结果

测试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}}

正如您所看到的, getRouterState 选择器不仅检索状态的 router 切片,而是检索包含整个 routerState authState State 的对象 . router和auth是此对象的子级 . 因此选择器无法检索正确的切片 .

测试2

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

undefined - TypeError:无法读取未定义的属性“state”

测试3

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

undefined TypeError:无法读取未定义的属性'state'TypeError:无法读取{auth:{}的属性'loggedIn',router:{}}

注意

请注意语法

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

如果我们想要使用多个reducer组合选择器,似乎是强制性的 . 我们可以使用 forRoot(reducers) 但是我们不能只测试路由器选择器 . 国家的其他部分将不存在 .

例如,如果我们需要测试:

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

我们需要路由器和auth . 我们找不到合适的测试设置,允许我们使用 AuthStateRouterState 来测试这样的组合选择器 .

问题

如何设置此测试以便我们基本上可以测试我们的选择器?

当我们运行应用程序时,它完美运行 . 所以问题只在于测试设置 .

我们认为使用真实路由器设置testBed可能是错误的想法 . 但我们很难模拟routerSelector(仅)并给它一个模拟的路由器状态片仅用于测试目的 .

仅模拟这些路由器选择器真的很难 . Spy store.select 很容易,但在 store.select(routerSelectorMethod) 上进行 Spy 活动,方法作为参数变得一团糟 .

2 回答

  • 1

    现在,您可以使用 projector 属性模拟选择器依赖项:

    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);
      });
    
    });
    

    取自official docs

  • 2

    我自己也在努力解决这个问题,路由器状态的'state'属性是未定义的 . 我找到了适合我的解决方案是调用router.initialNavigation()来启动RouterTestingModule,后者又设置了路由器存储 .

    在我的情况下,我需要测试一个使用根存储选择器和功能存储选择器的CanActivate防护 . 下面的测试模块设置适用于我:

    describe('My guard', () => {
    
       let myGuard: MyGuard;
       let router: Router;
       let store: Store<State>;
    
       beforeEach(async(() => {
           TestBed.configureTestingModule({
               imports: [
                   RouterTestingModule.withRoutes([
                       {
                           path: '',
                           redirectTo: 'one',
                           pathMatch: 'full'
                       },
                       {
                           path: 'one',
                           component: MockTestComponent
                       },
                       {
                           path: 'two',
                           component: MockTestComponent
                       }
                   ]),
                   StoreModule.forRoot({
                       ...fromRoot.reducers,
                       'myFeature': combineReducers(fromFeature.reducers)
                   }),
                   StoreRouterConnectingModule.forRoot({
                       stateKey: 'router', // name of reducer key
                   }),
               ],
               declarations: [MockTestComponent],
               providers: [MyGuard, {provide: RouterStateSerializer, useClass: CustomSerializer}]
           }).compileComponents();
    
           myGuard = TestBed.get(MyGuard);
           router = TestBed.get(Router);
           store = TestBed.get(Store);
           spyOn(store, 'dispatch').and.callThrough();
           router.initialNavigation();
       }));
    });
    

相关问题