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

Time:01-13

I am trying to unit test a service I have written. Most of the tests are passing but my last test is failing despite my mock set up being the same as one of my working tests.

Here is my service. You will notice that I import other services to make API calls and to raise a snackbar. Also in my service I have logic to determine which API should be called:

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

This works great and now I need to write my unit tests:

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

As you can see I am setting up spies for my API service call and I am setting return values for the API calls. The problem is with the second test, the mocked service/spy does get called but the .subscribe method doesn't seem to be actioned and thus in the second test my code never steps inside the apiCall().subscribe callback (see the comment in the service) and the test fails here:

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

with the error:

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

I am unsure why this works for the first test but not the second. I have tried using ngOnDestroy, I have tried changing the set up of my spies and mocks but nothing seems to fix the second unit test.

CodePudding user response:

it seems the test is ok, but the implementation is not.

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

I believe this code should also fail on runtime, because in the second case apiCall = someObservable; and you can't just call it apiCall()

  •  Tags:  
  • Related