'Spy on the result of an Observable Subscription with Jasmine

I am Jasmine unit testing an angular component, which uses Observables. My component has this lifecycle hook that I am testing:

ngOnInit() {
  this.dataService.getCellOEE(this.cell).subscribe(value => this.updateChart(value));
}

I have a test that ensures that getCellOEE has been called, but now I want to check that updateChart is called when the observable resolves with a new value. This is what I have so far:

let fakeCellService = {
  getCellOEE: function (value): Observable<Array<IOee>> {
    return Observable.of([{ time: moment(), val: 67 }, { time: moment(), val: 78 }]);
  }
};

describe('Oee24Component', () => {
  let component: Oee24Component;
  let service: CellService;
  let injector: Injector;
  let fixture: ComponentFixture<Oee24Component>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [Oee24Component],
      providers: [{ provide: CellService, useValue: fakeCellService }]
    })
      .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(Oee24Component);
    component = fixture.componentInstance;
    injector = getTestBed();
    service = injector.get(CellService)
    fixture.detectChanges();
    spyOn(service, 'getCellOEE').and.returnValue({ subscribe: () => { } });
    spyOn(component, 'updateChart');
  });

  it('should get cell oee on init', () => {
    component.ngOnInit();
    expect(service.getCellOEE).toHaveBeenCalled();
  });

  it('should update chart on new data', () => {
    component.ngOnInit();
    expect(component.updateChart).toHaveBeenCalled();
  });
});

However, I get the error:

chrome 56.0.2924 (Windows 10 0.0.0) Oee24Component should update chart on new data FAILED

Expected spy updateChart to have been called.

Presumably this is a timing issue because the observable hasn't necessarily resolved when the test checks? If that is the case, how do I set this up correctly?

Update:

Here is my component:

@Component({
  selector: 'app-oee24',
  templateUrl: './oee24.component.html',
  styleUrls: ['./oee24.component.css']
})
export class Oee24Component implements OnInit {
  public barChartData: any[] = [{ data: [], label: 'OEE' }];

  constructor(public dataService: CellService) { }

  ngOnInit() {
    this.dataService.getCellOEE(this.cell).subscribe(value => this.updateChart(value));
  }

  updateChart(data: Array<IOee>) {
    this.barChartData[0].data = data.map(val => val.val);
  }
}
 


Solution 1:[1]

Did you ever come up with a solution? What about using the jasmine-marbles package and the complete event?

it('should update chart on new data', () => {
    const obs$ = cold('--a-|');
    spyOn(service, 'getCellOEE').and.returnValue(obs$);
    component.ngOnInit(); 
    obs$.subscribe({
        complete: () => {
            expect(component.updateChart).toHaveBeenCalledWith('a');
        }
    });
});

Solution 2:[2]

Not sure if this is the best way to do it, but I've seen it working on a project I'm working on. The approach is basically get a reference to the callback function provided to the subscribe method, and call it manually to simulate the observer emitting a value:

it('should update chart on new data', () => {
    component.ngOnInit();

    // this is your mocked observable
    const obsObject = service.getCellOEE.calls.mostRecent().returnValue;

    // expect(obsObject.subscribe).toHaveBeenCalled() should pass

    // get the subscribe callback function you provided in your component code
    const subscribeCb = obsObject.subscribe.calls.mostRecent().args[0];

    // now manually call that callback, you can provide an argument here to mock the "value" returned by the service
    subscribeCb(); 

    expect(component.updateChart).toHaveBeenCalled();
  });

Solution 3:[3]

Instead of

spyOn(service, 'getCellOEE').and.returnValue({ subscribe: () => { } });

You could try

spyOn(service, 'getCellOEE').and.returnValue( {subscribe: (callback) => callback()});

Solution 4:[4]

fixture.detectChanges triggers ngOnInit. so, no need to call ngOnInit manually if fixture.detectChanges was executed.

It's a bad practice to check if the method was called. Instead, it is more reliable to check the expected results of the code execution.

There is no need in the line spyOn(service, 'getCellOEE').and.returnValue({ subscribe: () => { } }); because fakeCellService already mocks properly the service.

The tested code is async, so we need to wait till it would be executed. await fixture.whenStable(); does exactly this.

So, the resulting test:

const fakeData = [{ time: moment(), val: 67 }, { time: moment(), val: 78 }];
const expectedChartData = [67, 78];
const fakeCellService = {
  getCellOEE: function (value): Observable<Array<IOee>> {
    return Observable.of(fakeData);
  }
};

describe('Oee24Component', () => {
  let component: Oee24Component;
  let service: CellService;
  let injector: Injector;
  let fixture: ComponentFixture<Oee24Component>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [Oee24Component],
      providers: [{ provide: CellService, useValue: fakeCellService }]
    })
      .compileComponents();
  }));

  beforeEach(async () => {
    fixture = TestBed.createComponent(Oee24Component);
    component = fixture.componentInstance;
    fixture.detectChanges();
    await fixture.whenStable();
  });

  it('maps and saves value from the CellService.getCellOEE to barChartData[0].data when initialized', () => {
    expect(component.barChartData[0].data).toEqual(expectedChartData);
  });
});

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Daniel Smith
Solution 2 Sil
Solution 3 ziale
Solution 4 IAfanasov