Angular Unit Test Mock Selector funktioniert nicht

Sep 21 2020

Ich versuche, einen Selektor in meinem Unit-Test wie folgt zu verspotten:

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

Hier ist die Komponentendatei

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, 0, 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;
  }
}

Aber wenn ich die Spezifikation starte, erhalte ich den Fehler als

TypeError: Die Eigenschaft 'Länge' von undefined kann nicht gelesen werden

Antworten

ShashankVivek Sep 21 2020 at 18:21

Ich denke, das Problem liegt in der Verspottung des Selektors provideMockStore. Ich kann sehen, dass Sie eine Menge this.selectedRows.length(wie in setEditBackgroundColor()) verwendet haben, die basierend auf dem ngRx Selector eingestellt werden.

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

für einen Selektor wie:

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

Versuchen Sie dies gemäß der erwarteten Ausgabe von select('devices')und ich denke, es sollte funktionieren.


Nebenbei bemerkt, versuchen Sie nicht, Funktionsaufrufe aus HTML wie in setEditBackgroundColor()und anderen zu tätigen. Dies wirkt sich auf die Leistung aus und wird in jedem ChangeDetection-Zyklus aufgerufen (versuchen Sie, console.logsolche Methoden einzufügen ). Sie werden mehrmals aufgerufen. Verwenden Sie besser einige map, um eine Objekteigenschaft festzulegen und sie dann in HTML zu rendern

Nidhinkumar Sep 24 2020 at 14:47

Anstelle von Selektoren habe ich den Speicher mit Anfangswerten wie unten eingestellt

 const mockInitialAppState = {
    devices: deviceState(),
  };

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

    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 },
        { provide: GridApi, useClass: MockGridApi },
        provideMockStore({
          initialState: { ...mockInitialAppState },
        }),
      ],
    }).compileComponents();
  });

Dadurch wird der Test bestanden. Um den Anfangszustand zu umgehen, muss ich die ausgewählten Zeilen nicht initialisieren