可观察 mock/spy 在 Angular 单元测试中不一致 - 第一次调用成功,第二次失败

Observable mock/spy not consistent in Angular unit test - successful on first call, fails on second

我正在尝试对我编写的服务进行单元测试。大多数测试都通过了,但我的最后一个测试失败了,尽管我的模拟设置与我的一个工作测试相同。

这是我的服务。您会注意到我导入了其他服务来进行 API 调用和设置小吃店。同样在我的服务中,我有逻辑来确定应该调用哪个 API:

showMarkOffline(incident: any): void {
    let name;
    let apiCall;

    if (incident.sessionType === 'network') {
      name = incident.networkName;
      const nodes = (incident.nodes || []).map((node) => node.id);
      apiCall = () => this.hubsApiService.markOffline(incident.id, nodes);
    } else {
      name = (incident && incident.workerName) ? incident.workerName : incident.id;
      const gatewayId = (incident && incident.gatewayId) ? incident.gatewayId : null;
      apiCall = this.phonesApiService.markOffLine(gatewayId, incident.id);
    }

    const dialogRef = this.modalWrapperService.openConfirmDialog('ns.common:markOfflineDialog.title',
      ['ns.common:markOfflineDialog.content', { 0: name }],
      'ns.common:markOfflineDialog.ok',
      'ns.common:cancel');

    dialogRef.afterClosed().subscribe((confirmation: boolean) => {
      if (confirmation) {
        apiCall().subscribe(() => {
        // the second test doesn't seem to get here
          this.snackbarWrapperService
            .openSuccess('ns.common:markOfflineDialog.passed', { 0: name });
        }, () => {
          this.snackbarWrapperService
            .openError('ns.common:markOfflineDialog.failed', { 0: name });
        });
      }
    });
}

效果很好,现在我需要编写单元测试:

describe('IncidentsService', () => {
  let service: IncidentsService;
  const mockHubsApiService = {
    markOffline: jest.fn()
  };

  const mockPhonesApiService = {
    markOffLine: jest.fn()
  };

  const mockModalDialogWrapperService = {
    openConfirmDialog: jest.fn()
  };

  const mockSnackBarWrapperService = {
    openSuccess: jest.fn(),
    openError: jest.fn()
  };

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [{
        provide: HubsApiService,
        useValue: mockHubsApiService
      }, {
        provide: PhonesApiService,
        useValue: mockPhonesApiService
      }, {
        provide: ModalDialogWrapperService,
        useValue: mockModalDialogWrapperService
      }, {
        provide: SnackBarWrapperService,
        useValue: mockSnackBarWrapperService
      }]
    });
    service = TestBed.inject(IncidentsService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  
  describe('showMarkOffline', () => {
    // this test passes!!!! 
    it('should call hubsApiService.markOffline, openConfirmDialog and openSuccess', () => {
      const incident = {
        id: '12345',
        sessionType: 'network',
        networkName: 'RaduNetwork',
        nodes: [{ id: '1', foo: 'bar' }, { id: '2', foo: 'bar' }, { id: '3', foo: 'bar' }, { id: '4', foo: 'bar' }]
      };

      const modalSpy = spyOn(service.modalWrapperService, 'openConfirmDialog').and
        .returnValue({ afterClosed: () => of(true) });
      const apiSpy = spyOn(service.hubsApiService, 'markOffline').and
        .returnValue(of(true));
      const snackBarSpy = spyOn(service.snackbarWrapperService, 'openSuccess');

      service.showMarkOffline(incident);

      expect(modalSpy).toHaveBeenCalledWith('ns.common:markOfflineDialog.title',
        ['ns.common:markOfflineDialog.content', { 0: 'RaduNetwork' }],
        'ns.common:markOfflineDialog.ok',
        'ns.common:cancel');
      expect(apiSpy).toHaveBeenCalledWith('12345', ['1', '2', '3', '4']);
      expect(snackBarSpy).toHaveBeenCalledWith('ns.common:markOfflineDialog.passed', { 0: 'RaduNetwork' });
    });

    // this test fails!
    it('should call phonesApiService.markOffline, openConfirmDialog and openSuccess',() => {
      const incident = {
        id: '12345',
        workerName: 'workerName',
        gatewayId: '54321',
        sessionType: 'whatever'
      };

      const modalSpy = spyOn(service.modalWrapperService, 'openConfirmDialog').and
        .returnValue({ afterClosed: () => of(true) });
      const apiSpy = spyOn(service.phonesApiService, 'markOffLine').and
        .returnValue(of(true));;
      const snackBarSpy = spyOn(service.snackbarWrapperService, 'openSuccess');

      service.showMarkOffline(incident);

      expect(modalSpy).toHaveBeenCalledWith('ns.common:markOfflineDialog.title',
        ['ns.common:markOfflineDialog.content', { 0: 'workerName' }],
        'ns.common:markOfflineDialog.ok',
        'ns.common:cancel');
      expect(apiSpy).toHaveBeenCalledWith('54321', '12345');
      // below is the failing test
      expect(snackBarSpy).toHaveBeenCalledWith('ns.common:markOfflineDialog.passed', { 0: 'workerName' });
    });
  });
});

如您所见,我正在为我的 API 服务调用设置间谍,并且我正在为 API 调用设置 return 值。问题出在第二次测试中,模拟的 service/spy 确实被调用但是 .subscribe 方法似乎没有被执行,因此在第二次测试中我的代码从未进入 apiCall().subscribe回调(见服务中的注释),这里测试失败:

expect(snackBarSpy).toHaveBeenCalledWith('ns.common:markOfflineDialog.passed', { 0: 'workerName' });

错误:

Error: expect(spy).toHaveBeenCalledWith(...expected)
Expected: "ns.common:markOfflineDialog.passed", {"0": "workerName"}
Number of calls: 0

我不确定为什么这对第一次测试有效,但对第二次测试无效。我试过使用 ngOnDestroy,我试过更改我的间谍和模拟的设置,但似乎没有任何东西可以修复第二个单元测试。

貌似测试没问题,执行起来不行

apiCall = () => this.hubsApiService.markOffline(incident.id, nodes); // passes
vs
apiCall = this.phonesApiService.markOffLine(gatewayId, incident.id); // does not.

我相信这段代码在运行时也应该会失败,因为在第二种情况下 apiCall = someObservable; 你不能只称它为 apiCall()