如何检查内部组件的方法在测试期间被调用?

How to check inner component's method is called during tests?

我有一个通过 HexesService 调用外部 API 的组件 (MapComponent)。此 API 调用的数据结果加载到某些按钮(查看地图、编辑地图)。我有另一个内部组件 (HexesComponent),单击“查看”按钮会调用此内部组件(这又会进行另一个 API 调用并显示 HexesComponent 内的数据)。

我创建了一个模拟外部服务的测试。我的测试检查在加载主要组件后,它会调用模拟服务并正确加载数据(所有按钮都已填充)。这工作得很好。

接下来我要检查单击按钮是否会调用 HexesComponent 的方法。

我的问题是我的内部组件总是未定义。 我已经为我的内部组件创建了一个存根,但这没有帮助:即使是存根也是空的。

问题 #1: 我做错了什么?我已经尝试对 'beforeEach' 中的方法使用异步,但这没有帮助。

不用说 'tests' 之外,该功能运行得非常好。

问题 #2: 如何验证单击按钮会导致调用 HexComponent 的 'loadMap' 方法?

这是我的测试代码:

class MockHexesService extends HexesService {
  getDataForButtons(){
    return of(...);
  }
  ...
}

@Component({ selector: 'app-hex', template: '' })
class HexStubComponent {
   public loadMap = jasmine.createSpy('loadMap');
}

describe('MapsComponent', () => {
  let component: MapsComponent;
  let hexComponent: HexStubComponent;
  let fixture: ComponentFixture<MapsComponent>;
  let componentService: HexesService;
  let hexComponentSpy: any;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientModule],
      declarations: [MapsComponent, HexStubComponent],
      providers: [HexesService],
    }).compileComponents();

    TestBed.overrideComponent(MapsComponent, {
      set: {
        providers: [{ provide: HexesService, useClass: MockHexesService }],
      },
    });

    fixture = TestBed.createComponent(MapsComponent);
    component = fixture.componentInstance;
    componentService = fixture.debugElement.injector.get(HexesService);

    fixture.detectChanges();

    hexComponent = fixture.debugElement.injector.get(HexStubComponent);
    hexComponentSpy = spyOn(hexComponent, 'loadMap');
  });

  it('Loaded map descriptions should load data for buttons', () => {
    const tblRows =
      fixture.debugElement.nativeElement.querySelectorAll('table tr');
    const cells0 = tblRows[0].querySelectorAll('td');
    expect(cells0[0].innerHTML).toBe('Map 1');
    ... checking other buttons are populated properly

    expect(component.hexComponent).toBeTruthy(); // - this fails here
  });

  it('Click on "View" button should load the map', async(() => {
    //spyOn(component.hexComponent.loadMap)
    const btns = fixture.debugElement.nativeElement.querySelectorAll('button');
    expect(btns.length).toBe(6);
    const btnView = btns[0];
    expect(btnView.innerHTML).toBe('View Map');
    btnView.click();

    fixture.whenStable().then(() => {
      expect(component.hexComponent).toBeTruthy(); // - this fails 
      // and how do I check that 'loadMap' method of hexComponent is called with proper data?
    });
  }));
});

maps.component.html的重要部分:

...
<tr *ngFor="let d1 of data">
  ...    
  <button (click)="loadMap(d1.mapId)">View Map</button>
  ...
</tr>
<app-hex id="hexComponent"></app-hex>

maps.component.ts的重要部分:

export class MapsComponent implements OnInit {
  @ViewChild(HexComponent) hexComponent: HexComponent;

  constructor(public ngZone: NgZone, private hexesService: HexesService) {}

  ngOnInit(): void {
    this.hexesService
      .getDataForButtons()
      .subscribe((data: any) => this.populateButtons(data));
  }

  loadMap(mapId): void {
    this.hexComponent.loadMap(mapId);
  }

P.S。我发现了一个类似的问题 (How can I use a fake/mock/stub child component when testing a component that has ViewChildren in Angular 10+?),它建议使用 ng-mocks,但无法使它们工作。

P.P.S。我试过使用

@Component({ selector: 'app-hex', template: '' })
class HexStubComponent {
  //public loadMap(mapId: number) {}
  public loadMap = jasmine.createSpy('loadMap');
}

并在 'TestBed.configureTestingModule' 的声明中使用 HexStubComponent,但我的测试给出了错误:

NullInjectorError: R3InjectorError(DynamicTestModule)[HexStubComponent -> HexStubComponent]: NullInjectorError: No provider for HexStubComponent!

不知道如何解决。

我决定再试一次,让它与 ng-mocks 一起工作,而不是推动使用 Stabs 的选项。

显然,我的问题接近 好的指导是 https://www.npmjs.com/package/ng-mocks/v/11.1.4 不同的是在我的情况下,我还需要确保调用子组件的一些方法。

为了帮助其他人(也许将来我自己),这里列出了我在途中遇到的问题。

  • 使用“npm install ng-mocks --save-dev”命令安装 ng-mocks

  • 添加导入行(很明显,但 VS Code 没有自动建议这些,我努力了解如何访问我需要的确切 类):

    import { MockComponent, MockInstance, ngMocks } from 'ng-mocks';
    
  • 在 TestBed.configureTestingModule 的异步版本中添加您的组件模拟:

    declarations: [MapsComponent, MockComponent(HexComponent)],
    
  • 在您需要检查的组件和方法上创建间谍:

    beforeEach(() => MockInstance(HexComponent, 'loadMap', jasmine.createSpy()));
    
  • 验证:

    it('Click on "View" button should load the map', async(() => {
      const hexComponent = ngMocks.findInstance(HexComponent);
      expect(hexComponent).toBeDefined();
      expect(hexComponent.loadMap).not.toHaveBeenCalled();
    
      const btns = fixture.debugElement.nativeElement.querySelectorAll('button');
      expect(btns.length).toBe(6);
      const btnView = btns[0];
      expect(btnView.innerHTML).toBe('View Map');
    
      btnView.click();
    
      fixture.whenStable().then(() => {
        expect(hexComponent.loadMap).toHaveBeenCalled();
      });
    }));
    

我唯一没有解决的问题是模拟 HexComponent 使用的一些服务。我稍后再试。

Angular @ViewChild 和其他查询基于 idctor.

因为 HexStubComponent 不是 HexComponent,查询无法找到它。

如您所说,为了修复它,您可以使用 ng-mocks and MockComponent or MockBuilder,然后库将完成所有设置。

或者你可以告诉Angular当请求HexComponent时应该提供HexStubComponent:

@Component({ selector: 'app-hex', template: '',

  // this is the change
  providers: [
    {
      provide: HexComponent,
      useExisting: HexStubComponent,
    },
  ],
  // the end of the change

})
class HexStubComponent {
   public loadMap = jasmine.createSpy('loadMap');
}

这就是 ng-mocks 在幕后所做的,应该可以帮助您实现所需的行为。

关于第二个问题。这听起来像是对 HexComponent 本身的测试,因为期望的期望是确保 HexComponent 行为如何,而这里我们有它的模拟。

在当前测试中,我建议覆盖 getDataForButtons 已在点击时被调用,但不是它的作用。它所做的应该是一个不同的测试套件。