Search code examples
angularjasminemockingkarma-jasmineangular-test

Jasmine spy is not overwriting mock implementation


In the ngOnInit method of the class I am testing I call a function of a service which retruns an observable. I have implemented a mock for the service, but I'm trying to use a spy for this exact test case. In my understanding the spy would overwrite the mock implementation unless I call ".and.callThrough()" on the spy. The problem is that everytime the mock implementation still gets executed although I set up a spy for the function.

I tried moving the spy into the beforeEach section which did not help. Also I tried to use the spy without the ".and.callFake()" extension. But it didn't help.

spec.ts file:

fdescribe('AppComponent', () => {
  let fixture;
  let component;
  let dataServiceMock: DataServiceMock;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [RouterTestingModule],
      declarations: [AppComponent],
      providers: [{ provide: DataService, useClass: DataServiceMock }],
      schemas: [CUSTOM_ELEMENTS_SCHEMA]
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();

    dataServiceMock = TestBed.get(DataService);
  });


  fit('should not show navigation if not logged in', async(() => {
   spyOn(dataServiceMock,'getCurrentUser').and.callFake(() => {
     console.log('IN CALL FAKE')
     throwError(new Error('induced error'))
   });
 }));

implementation of service mock:

export class DataServiceMock {
  currentUser: User;

  private createValidUser() {
    let validUser = new User();
    validUser.username = 'valid';
    validUser.password = 'valid';
    validUser.role = 'valid';
    this.currentUser = validUser;
  }

  public getCurrentUser(): Observable<User> {
    this.createValidUser();
    return of(this.currentUser);
  }

ngOnInit of component that is tested:

ngOnInit(): void {
  this.dataService.getCurrentUser().subscribe(user => {
    this.currentUser = user;
    console.log('received user:', this.currentUser)
  })
}

I expect that the console log prints out "IN CALL FAKE" and throws the "induced error" but instead the console prints out "received user:" and the validUser that is created in the service mock.


Solution

  • This is just a timing problem. In your beforeEach() you are executing fixture.detectChanges(). This executes ngOnInit(), see the docs for details. So the solution is to NOT call fixture.detectChanges() there, but move it into the spec AFTER you have changed the return from getCurrentUser.

    Here is a working StackBlitz showing this test working. I also changed a couple of more details to get a working test:

    • Your callFake wasn't actually returning anything. It looked like you meant to return throwError(), however that caused further issues since you don't actually have any Observable error handling in your component so it makes no sense to test for that.
    • I added a fake return return of({username: 'test'}) just to allow the .subscribe() within your component's ngOnInit() to set up something that could be tested - I then set up a simple expect which tested that component.currentUser.username was set properly.
    • I removed the unnecessary async() wrapper you had around this spec - since you are using synchronous Observables (created with of()) for testing, there is no need for this.

    Here is the new spec from that StackBlitz:

    it('should not show navigation if not logged in', () => {
      spyOn(dataServiceMock,'getCurrentUser').and.callFake(() => {
        console.log('IN CALL FAKE')
        //  throwError(new Error('induced error'))
        return of({username: 'test'})
      });
      fixture.detectChanges();
      expect(component.currentUser.username).toEqual('test');
    });
    

    I hope this helps.