无法使用 Karma 测试测试 MatTable 填充行

Unable to test MatTable filling rows with Karma test

我希望得到帮助理解我的代码方法有什么问题。我试图模仿 this tutorial about Harnesses

的动态

我有以下 Angular 组件,它是绑定到远程服务的直接数据 table (MatTable)。它不是 GoT 角色,而是作为管理屏幕的一部分与系统角色一起工作

roles.component.html

<div class="pb-3">
  <button class="btn btn-success" (click)=add()>Add</button>
</div>
<table mat-table [dataSource]="dataSourceRole" matSort class="mat-table w-100">
  <ng-container matColumnDef="id">
    <th mat-header-cell *matHeaderCellDef>ID</th>
    <td mat-cell *matCellDef="let dto">{{dto.roleId}}</td>
  </ng-container>
  <ng-container matColumnDef="description">
    <th mat-header-cell *matHeaderCellDef>Name</th>
    <td mat-cell *matCellDef="let dto">{{dto.description}}</td>
  </ng-container>
  <ng-container matColumnDef="created">
    <th mat-header-cell *matHeaderCellDef>Created<th>
    <td mat-cell *matCellDef="let dto">{{dto.created | date:'dd/MM/yyyy HH:mm'}}</td>
  </ng-container>
  <ng-container matColumnDef="modified">
    <th mat-header-cell *matHeaderCellDef>Modified</th>
    <td mat-cell *matCellDef="let dto">{{dto.modified | date:'dd/MM/yyyy HH:mm'}}</td>
  </ng-container>
  <ng-container matColumnDef="bottoni">
    <th mat-header-cell *matHeaderCellDef>Actions</th>
    <td mat-cell *matCellDef="let dto">
      <button mat-button (click)=edit(dto.roleId)>
        <fa-icon icon="pencil-alt"></fa-icon>
      </button>
      <button mat-button (click)=delete(dto.roleId)>
        <fa-icon icon="recycle"></fa-icon>
      </button>
    </td>
  </ng-container>
  <tr mat-header-row *matHeaderRowDef="columnsToDisplay" style="width: available;"></tr>
  <tr mat-row *matRowDef="let myRowData; columns: columnsToDisplay"></tr>
</table>

roles.component.ts

import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {MatSort} from '@angular/material/sort';
import {MatTableDataSource} from '@angular/material/table';
import {Router} from '@angular/router';
import {RoleDto} from 'src/app/services/model/role-dto';
import {RoleService} from 'src/app/services/role.service';
import {Subscription} from "rxjs";

@Component({
  selector: 'app-roles',
  templateUrl: './roles.component.html',
  styleUrls: ['./roles.component.scss']
})
export class RolesComponent implements OnInit, OnDestroy {


  columnsToDisplay: string[] = ['id', 'description', 'created', 'modified', 'bottoni'];
  dataSourceRole: MatTableDataSource<RoleDto> = new MatTableDataSource<RoleDto>([]);
  @ViewChild(MatSort) sort!: MatSort;
  private roles$?: Subscription;

  constructor(private roleService: RoleService,
              private router: Router,
  ) {
    this.dataSourceRole.sort = this.sort;
  }

  ngOnInit(): void {
    this.getRoles();
  }

  ngOnDestroy() {
    this.roles$?.unsubscribe();
  }

  getRoles(): void { //TODO: unsubscribe if needed. I found this issue while writing this post
    this.roles$ = this.roleService.getRoles()
      .subscribe({
        next: rolesData => this.dataSourceRole.data = rolesData,
        error: error => console.error(error),
      });
  }

  edit(roleID: string): void {
    this.router.navigate(['/roles', 'details', roleID]);
  }

  add(): void {
    this.router.navigate(['/roles', 'new']);
  }

  delete(roleID: any): void {
    this.roleService.deleteRole(roleID)
      .subscribe({
        next: _ => this.getRoles(),
        error: error => console.error(error),
      });

  }

}

roles.component.spec.ts

import {ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing';

import {RolesComponent} from './roles.component';
import {RoleService} from "../../services/role.service";
import {BehaviorSubject, Subject} from "rxjs";
import {RoleDto} from "../../services/model/role-dto";
import {HarnessLoader} from "@angular/cdk/testing";
import {TestbedHarnessEnvironment} from "@angular/cdk/testing/testbed";
import {MatTableHarness} from "@angular/material/table/testing";
import {RouterTestingModule} from "@angular/router/testing";
import {FormRoleComponent} from "./form-role/form-role.component";
import {Router} from "@angular/router";
import SpyObj = jasmine.SpyObj;

describe('RolesComponent', () => {
  let component: RolesComponent;
  let fixture: ComponentFixture<RolesComponent>;
  let roleService: RoleService;
  let roleServiceSpy: SpyObj<RoleService>;
  let loader: HarnessLoader;
  let router: Router;
  let returnedRoles = new Subject<RoleDto[]>(); //Explanation 1

  beforeEach(() => {
    roleServiceSpy = jasmine.createSpyObj(RoleService, ['getRoles', 'getRole', 'createRole', 'deleteRole', 'updateRole']);
    roleService = roleServiceSpy as RoleService;
    roleServiceSpy.getRoles.and.returnValue(returnedRoles);
  });

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [RolesComponent],
      providers: [
        {provide: RoleService, useValue: roleService},
      ],
      imports: [RouterTestingModule.withRoutes([
        {
          path: 'roles/new',
          component: FormRoleComponent
        },
        {
          path: 'roles/:id',
          component: FormRoleComponent
        },
        {
          path: 'roles',
          component: RolesComponent,
        },])]
    })
      .compileComponents();
  });

  beforeEach(() => {
    router = TestBed.inject(Router);
    fixture = TestBed.createComponent(RolesComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
    loader = TestbedHarnessEnvironment.loader(fixture);
  });

  afterEach(() => fixture.destroy());

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  /**
   * GIVEN
   * RoleService.getRoles returns 3 roles
   *
   * WHEN
   * Component is loaded
   *
   * THEN
   * Component displays 3 data rows
   * Rows contain text matching the role ID in its returned order
   */
  it('should display the rows when data is present', async () => {
    let roles: RoleDto[] = [
      {roleId: 'ADMIN', description: 'Admin role', created: new Date(), creator: 0, modified: new Date(), modifier: 0},
      {roleId: 'USER', description: 'User role', created: new Date(), creator: 0, modified: new Date(), modifier: 0},
      {roleId: 'READER', description: 'Reader role', created: new Date(), creator: 0, modified: new Date(), modifier: 0},
    ];
    returnedRoles.next(roles); //Explanation 1

    fixture.detectChanges();

    let tableHarness = await loader.getHarness(MatTableHarness);
    let rows = await tableHarness.getRows();
    expect(rows.length).toBe(roles.length); //Question
    for (const row of rows) { //This is untested and may be totally wrong
      const index = rows.indexOf(row);
      let text = (await row.getCellTextByIndex({columnName: 'ID'})).join(' ');
      let expected: string = roles[index].roleId!;
      expect(text).toContain(expected);
    }

  });
});

解释 1

我努力让数据源工作了几个小时。在我的第一个测试版本中,我只是简单地将 getRoles() 方法模拟为 return Observable.of(thatArray) 但最后我在调试过程中发现 ngOnInit 被提前调用了 veeeeeeery。太早了,我没有时间模拟 ngOnInit 完成的方法及其回调。

所以我决定 return 一个我可以从我的测试中控制的 Observable,它会在我调用 next.

后触发回调

问题

我的测试代码不工作。该组件在具有实时远程服务器的实时应用程序中调用时工作。

在组件测试中,rows是一个空数组。

我也试过了

最初将 Subject 更改为 BehaviourSubject return 一个角色的数组没有任何改变(测试结果为 0 行)。

使用 fakeAsync 没有改变。线束测试似乎不需要它,因为它们都是异步的。

评论

我正在努力向我的编码人员提供有关以正确方式使用自动化测试的工作示例,并且我已经花了足够的时间来使测试工作 学习如何教我如何编写测试代码,我的 PM

可以随时召唤我

We'll do the tests manually the old school way.

最后问题是没有导入 MatTableModule,也许 MaterialModule 也是。当我编辑问题时,我遇到了一些编译问题,未能 运行 正确的测试代码。

修复后,一切正常。修复是将 MatTableModuleMaterialModule 添加到 TestBed

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [RolesComponent],
      providers: [
        {provide: RoleService, useValue: roleService},
      ],
      imports: [
        CommonModule,
        AmlcFontAwesomeModule,
        MaterialModule,
        MatTableModule,
        RouterTestingModule.withRoutes([
          {
            path: 'roles/new',
            component: FormRoleComponent
          },
          {
            path: 'roles/:id',
            component: FormRoleComponent
          },
          {
            path: 'roles',
            component: RolesComponent,
          },])]
    })
      .compileComponents();
  });