角单元测试模拟选择器不起作用

问题描述

我正在尝试在单元测试中模拟一个选择器,如下所示:

describe('Device List Component',() => {
  let component: ListComponent;
  let fixture: ComponentFixture<ListComponent>;
  let deviceServiceMock: any;
  let mockStore: MockStore<any>;
  let devices;


  beforeEach(async(() => {
    deviceServiceMock = jasmine.createSpyObj('DevicesService',['fetchDevices']);
    deviceServiceMock.fetchDevices.and.returnValue(of(deviceState()));


    Testbed.configureTestingModule({
      declarations: [
        ListComponent,MockComponent(DataGridComponent),],imports: [
        RouterTestingModule,MockModule(SharedModule),ToastrModule.forRoot({
          preventDuplicates: true,closeButton: true,progressBar: true,}),TranslateModule.forRoot({
          loader: { provide: TranslateLoader,useClass: JsonTranslationLoader },providers: [
        { provide: DevicesService,useValue: deviceServiceMock },{ provide: ColumnApi,useClass: MockColumnApi },provideMockStore(),}).compileComponents();
  }));

  beforeEach(() => {
    fixture = Testbed.createComponent(ListComponent);
    component = fixture.componentInstance;
    mockStore = Testbed.get(MockStore);
    component.columnApi = Testbed.get(ColumnApi);
    devices = mockStore.overrideselector('devices',deviceState());
    fixture.detectChanges();
  });
});

这是组件文件

export class ListComponent implements OnInit,OnDestroy {
  columnDefs: DeviceColumns[];
  defaultColumnDefs: any;
  gridApi: any;
  columnApi: any;
  overlaynorowstemplate: string;
  rowData: DeviceData[] = [];
  pagination: DevicePagination;
  globalSearch: string;
  hasFloatingFilter: boolean;
  frameworkComponents: any;
  dropdownSettings: any = {};
  dropDownList: Columns[] = [];
  selectedItems: Columns[] = [];
  shuffledColumns: any = [];
  selectedRows: DeviceData[] = [];
  rowSelection: string;
  bsModalRef: BsModalRef;
  isColumnsSorting: boolean;
  hasRowAnimation = true;
  multiSortKey = 'ctrl';
  changeDetectorRef: ChangeDetectorRef;
  deviceDeleteSubscription: Subscription;

  currentPage = new Subject<number>();
  search = new Subject<string>();
  subscription: Subscription;

  constructor(
    private router: Router,private devicesService: DevicesService,public store: Store<any>,private toast: ToastrService,private ngzone: ngzone,private translateService: TranslateService,private globalTranslate: GlobalLanguageService,private modalService: BsModalService,changeDetectorRef: ChangeDetectorRef
  ) {
    this.translateService.stream(['DEVICES.LIST','MULTISELECT']).subscribe((translations) => {
      const listTranslations = translations['DEVICES.LIST'];
      const multiSelectTranslations = translations['MULTISELECT'];
      this.overlaynorowstemplate = `<span class="ag-overlay-loading-center">${listTranslations.NODEVICE}</span>`;
      this.dropdownSettings.selectAllText = multiSelectTranslations.SELECTALL;
      this.dropdownSettings.unSelectAllText = multiSelectTranslations.deselectALL;
    });
    this.changeDetectorRef = changeDetectorRef;
    this.translateService.onLangChange.subscribe(() => {
      this.gridApi && this.gridApi.refreshHeader();
    });
  }

  ngOnInit() {
    this.loadStore();
    this.initializeColumns();
    this.pageSearch();
    this.dropdownSettings = {
      singleSelection: false,idField: 'field',textField: 'key',selectAllText: 'Select All',unSelectAllText: 'UnSelect All',itemsShowLimit: 3,allowSearchFilter: true,enableCheckAll: true,};
    this.store.dispatch(new DevicesActions.ClearCurretDevice());
  }
  loadStore() {
    this.store.pipe(select('devices')).subscribe((val) => {
      const deviceList = val.devices.map((d) => {
        return {
          ...d,is_online: d.is_online ? 'Active' : 'Inactive',};
      });
      this.rowData = deviceList;
      this.pagination = val.pagination;
      this.globalSearch = val.globalSearch;
      this.hasFloatingFilter = val.hasFloatingFilter;
      this.dropDownList = val.shuffledColumns;
      this.selectedItems = val.shuffledColumns.filter((column: Columns) => !column.hide);
      this.selectedRows = val.selectedRows;
    });
  }
  initializeColumns() {
    this.columnDefs = [
      {
        headerName: 'S.No',translateKey: 'DEVICES.LIST.SNO',width: 100,resizable: false,sortable: false,suppressSizetoFit: true,valueGetter: (args) => this.getId(args),checkBoxSelection: (params) => {
          console.log('params.columnApi.getRowGroupColumns()',params.columnApi.getRowGroupColumns());
          return params.columnApi.getRowGroupColumns().length === 0;
        },headerCheckBoxSelection: (params) => {
          return params.columnApi.getRowGroupColumns().length === 0;
        },},...gridColumns,];
    this.columnDefs = map(this.columnDefs,(columnDef) => {
      return extend({},columnDef,{ headerValueGetter: this.localizeHeader.bind(this) });
    });
    this.shuffledColumns.push(this.columnDefs[0]);
    this.dropDownList.forEach((column,colIndex) => {
      this.columnDefs.forEach((data) => {
        if (data.field === column.field) {
          data.hide = column.hide;
          data.sort = column.sort;
          data.width = column.width;
          data.minWidth = column.minWidth;
          this.shuffledColumns.splice(colIndex + 1,data);
        }
      });
    });
    this.columnDefs = this.shuffledColumns;
    this.rowSelection = 'multiple';
    this.defaultColumnDefs = {
      suppressMenu: true,suppressMovable: true,sortable: true,resizable: true,};
    this.frameworkComponents = { FloatingFilterComponent: FloatingFilterComponent };
  }

  localizeHeader(params: any) {
    return this.globalTranslate.getTranslation(params.colDef.translateKey);
  }

  getId(args: any): any {
    return (
      this.pagination.per_page * this.pagination.prev_page + parseInt(args.node.rowIndex,10) + 1
    );
  }
  pageSearch() {
    this.subscription = this.search.subscribe((value) => {
      this.store.dispatch(new DevicesActions.GlobalSearch(value));
      if (value.length === 0) {
        this.clearSelectedRows();
        this.loadData();
      }
    });
  }
  OnGridReady(params) {
    this.gridApi = params.api;
    this.columnApi = params.columnApi;
    this.loadData();
  }
  loadData() {
    this.devicesService.fetchDevices(this.gridApi);
  }
  gotoAddDevice() {
    this.router.navigate(['/devices/new']);
  }
  searchDevices() {
    this.store.dispatch(new DevicesActions.UpdateCurrentPage(1));
    this.clearSelectedRows();
    this.loadData();
  }
  clearSelectedRows() {
    this.store.dispatch(new DevicesActions.ClearSelectedRows());
  }
  onItemSelect(item: DropDownColumns) {
    this.store.dispatch(new DevicesActions.ColumnSelect(item.field));
    this.columnApi.setColumnVisible(item.field,true);
  }
  onSelectAll(items: any) {
    this.store.dispatch(new DevicesActions.ColumnsSelectAll());
    items.map((item) => this.columnApi.setColumnVisible(item.field,true));
  }
  onItemUnSelect(item: DropDownColumns) {
    this.store.dispatch(new DevicesActions.Columndeselect(item.field));
    this.columnApi.setColumnVisible(item.field,false);
  }
  ondeselectAll() {
    this.store.dispatch(new DevicesActions.ColumnsdeselectAll());
    this.dropDownList.map((item) => this.columnApi.setColumnVisible(item.field,false));
  }
  SortedColumns(params: SortedColumns[]) {
    const columnsId = [];
    params.map((param: SortedColumns) => {
      columnsId.push(param.id);
    });
    const shuffledColumns = columnsId.map((columnId) =>
      this.dropDownList.find((data) => data.field === columnId)
    );
    this.store.dispatch(new DevicesActions.ShuffledColumns(shuffledColumns));
    this.columnApi.moveColumns(columnsId,1);
  }
  hasDevices() {
    return this.rowData.length > 0 ? true : false;
  }
  updatePage() {
    if (this.pagination.current_page.toString() === '') {
      this.pagination.current_page = 1;
    }
    this.store.dispatch(new DevicesActions.UpdateCurrentPage(this.pagination.current_page));
    this.clearSelectedRows();
    this.loadData();
  }
  prevIoUsPage() {
    this.store.dispatch(new DevicesActions.UpdateCurrentPage(this.pagination.prev_page));
    this.clearSelectedRows();
    this.loadData();
  }

  firstPage() {
    this.store.dispatch(new DevicesActions.UpdateCurrentPage(1));
    this.clearSelectedRows();
    this.loadData();
  }

  lastPage() {
    this.store.dispatch(new DevicesActions.UpdateCurrentPage(this.pagination.total_pages));
    this.clearSelectedRows();
    this.loadData();
  }

  nextPage() {
    if (!this.pagination.is_last_page) {
      this.store.dispatch(new DevicesActions.UpdateCurrentPage(this.pagination.next_page));
      this.clearSelectedRows();
      this.loadData();
    }
  }
  toggleFloatingFilter() {
    this.hasFloatingFilter = !this.hasFloatingFilter;
    this.store.dispatch(new DevicesActions.UpdateFloatingFilter(this.hasFloatingFilter));
    this.clearSelectedRows();
    this.gridApi.setRowData(this.rowData);
    if (!this.hasFloatingFilter) {
      this.gridApi.setFilterModel(null);
      this.store.dispatch(new DevicesActions.ClearColumnSearch());
      this.loadData();
    }
    setTimeout(() => {
      this.gridApi.refreshHeader();
    },0);
    window.location.reload();
  }
  isSortingEnabled() {
    this.isColumnsSorting = this.dropDownList.some((column) => column.sort !== '');
    return this.isColumnsSorting;
  }
  setSortingBackgroundColor() {
    return this.isColumnsSorting ? COLOR_PRIMARY : COLOR_SECONDARY;
  }

  setSortingIconColor() {
    return this.isColumnsSorting ? ICON_ENABLED : ICON_disABLED;
  }
  clearSort() {
    this.gridApi.setSortModel(null);
    this.store.dispatch(new DevicesActions.ClearColumnsSort());
    this.loadData();
  }
  resizeColumns() {
    const allColumnIds = [];
    this.columnApi.getColumnState().forEach((column) => {
      allColumnIds.push(column.colId);
    });
    this.columnApi.autoSizeColumns(allColumnIds,false);
  }
  onRowDataChanged() {
    if (this.gridApi) {
      this.gridApi.forEachNode((node: any) => {
        const selectNode = this.selectedRows.some((row) => row.id === node.data.id);
        if (selectNode) {
          node.setSelected(true);
        }
      });
    }
  }
  onSelectionChanged() {
    this.selectedRows = this.gridApi.getSelectedRows();
    console.log('selected rows',this.selectedRows);
    this.store.dispatch(new DevicesActions.UpdateSelectedRows(this.selectedRows));
    this.changeDetectorRef.detectChanges();
  }
  onSortChanged(params) {
    this.store.dispatch(new DevicesActions.UpdateColumnsSort(params));
    this.clearSelectedRows();
    this.loadData();
  }
  onColumnResized() {
    const updatedColumns: ColumnWidth[] = [];
    this.columnApi.getColumnState().forEach((column) => {
      updatedColumns.push({ field: column.colId,width: column.width });
    });
    this.store.dispatch(new DevicesActions.UpdateColumnWidth(updatedColumns));
  }
  gotoDetailView(params: any) {
    const id = params.id;
    this.ngzone.run(() => this.router.navigate(['/devices',id]));
  }
  isDeleteEnabled() {
    return this.selectedRows.length === 0 ? true : false;
  }
  setDeleteBackgroundColor() {
    return this.selectedRows.length !== 0 ? COLOR_PRIMARY : COLOR_SECONDARY;
  }

  setDeleteIconColor() {
    return this.selectedRows.length !== 0 ? ICON_ENABLED : ICON_disABLED;
  }

  openModal() {
    const initialState = {
      title: 'Delete Device',message: 'Do you really want to delete the device? This process cannot be undone',};
    if (this.selectedRows.length > 1) {
      (initialState.title = 'Delete Devices'),(initialState.message = `Do you really want to delete ${this.selectedRows.length} devices? This process cannot be undone`);
    }
    this.bsModalRef = this.modalService.show(ModalDeleteComponent,{ initialState });
    this.bsModalRef.content.delete.subscribe((canDelete: boolean) => {
      if (canDelete) {
        this.deleteDevices();
      }
      this.bsModalRef.hide();
    });
  }
  ngOnDestroy() {
    this.deviceDeleteSubscription?.unsubscribe();
  }
  deleteDevices() {
    const selectedIds = this.selectedRows.map((row) => row.id).toString();
    const params = {
      ids: selectedIds,};
    this.deviceDeleteSubscription = this.devicesService.deleteDevices(params).subscribe(
      (data) => {
        const ids = selectedIds.split(',').map(Number);
        this.clearSelectedRows();
        this.store.dispatch(new DevicesActions.DeleteDevices(ids));
        if (this.rowData.length === 0) {
          this.store.dispatch(new DevicesActions.UpdateCurrentPage(1));
          this.loadData();
        }
        this.toast.success('Deleted successfully');
        setTimeout(() => {
          window.location.reload();
        },500);
      },(error) => {
        this.toast.error(error.message);
      }
    );
  }

  editConfiguration() {
    this.store.dispatch(new DevicesActions.SetEditDevice(this.selectedRows[0]));
    this.router.navigate(['/devices','edit',this.selectedRows[0].id]);
  }

  isEditEnabled() {
    return this.selectedRows.length !== 1 ? true : false;
  }

  setEditBackgroundColor() {
    return this.selectedRows.length === 1 ? COLOR_PRIMARY : COLOR_SECONDARY;
  }

  setEditIconColor() {
    return this.selectedRows.length === 1 ? ICON_ENABLED : ICON_disABLED;
  }
}

但是当我运行规范时,我得到的错误

TypeError:无法读取未定义的属性“ length”

enter image description here

解决方法

我认为问题在于使用provideMockStore嘲笑选择器。我可以看到您使用了很多this.selectedRows.length(例如在setEditBackgroundColor()中),这些是基于ngRx选择器设置的。

providers: [
        { provide: DevicesService,useValue: deviceServiceMock },{ provide: ColumnApi,useClass: MockColumnApi },provideMockStore({
          selectors: [
            {
              selector: selectLoginPagePending,value: true
            }
          ]
        })
      ],

用于选择器,例如:

export const selectLoginPagePending = createSelector(
  selectLoginPageState,(state: State) => state.pending;
);

按照select('devices')的预期输出进行尝试,我认为它应该可以工作。


请注意,不要像在setEditBackgroundColor()和其他程序中那样从HTML进行函数调用,它会影响性能,并且会在每个ChangeDetection周期中调用(尝试放入console.log在这种方法中)。他们将被多次呼叫。最好使用一些map设置对象属性,然后在HTML上呈现

,

我没有使用选择器,而是使用以下初始值设置了商店

return BigList.Where(u => SmallList.Contains(u.UserName))
                    .OrderBy(u => u.UserName.Length)
                    .GroupBy(u => u.EmailAddress)
                    .Select(g => g.FirstOrDefault())
                    .Select(u => u.UserName).ToList();

哪个通过测试。通过初始状态,我不需要初始化selectedRows