4

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.

Cines of Lode
  • 119
  • 2
  • 7

1 Answers1

6

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.

dmcgrandle
  • 5,934
  • 1
  • 19
  • 38
  • This has been very helpful, thank you! I still have one more question. What I actually want to do is to not have a user logged in yet (= no return for the getCurrentUser function) and then check if a HTML element is redered in the DOM (It should not be there if the user is logged in). So, the getCurrentUser should not have returned anything yet in the moment when I call "expect" in the test. don't know how to do this (I'd guess that I need the async functionality for that). I tried returning a delayed Observable in the FakeCall but that is not working as expected. – Cines of Lode Jan 14 '19 at 09:41
  • Well, your component is not currently coded properly to make that happen. Right now you are calling `.subscribe()` on the return from `getCurrentUser()` - if you return nothing then you'll get a runtime error complaining that the subscribe method does not exist ... you could instead return an empty Observable, then test with an `if` clause within the `.subscribe()` whether anything was returned. As your component is currently written, it will not achieve what you are wanting. If you want help re-writing your code then please ask this as another question with all the details in it. :) – dmcgrandle Jan 14 '19 at 19:20
  • Actually my component is achieving exactly what I want: when i run the program, I start on a login screen and the component doesn't show the HTML element for navigating the web page. Only after I log in (the subscribe on the `getCurrentUser()` kicks in), the local variable `currentUser` gets set and the `*ngIf` (which is bound to this variable) in the HTML element for navigation gets activated and adds the element to the DOM. The question is only if I can also test this in a Jasmine test or not? – Cines of Lode Jan 15 '19 at 08:18
  • I think I may be mis-understanding the details because it seems to me this would cause a run-time error. I would suggest you fork the StackBlitz I made, then add all the details you are talking about (html template, etc) along with your attempt to make it work in the .spec - that should clarify it. The basic answer to your question is very likely "yes - Jasmine can test it". Regarding testing elements in the DOM - see the [Official Docs](https://angular.io/guide/testing#component-dom-testing) for a good discussion on how to achieve this. – dmcgrandle Jan 15 '19 at 17:30