angularunit-testingjasminetestbed

Spy service never being called in unit test (jasmine angular)


I'm trying to understand why my test doesn't work as expected. Here is the component:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [HomeService]
})
export class AppComponent {
  title = 'testing-angular';

  constructor (private service: HomeService ) {}

  CallHomeService(): void {
    this.service.DoStuff();
  }
}

and here the service:

@Injectable()
export class HomeService {

  constructor() { }

  DoStuff(): void {
    console.log('homeService has been called')
  }

}

I'd like only to check if DoStuff has been called, so I did this test:

describe('AppComponent', () => {
  let fixture: ComponentFixture<AppComponent>;
  let appComponent: AppComponent;
  let homeServiceMock: jasmine.SpyObj<HomeService>; // Use jasmine.SpyObj type for mock

  beforeEach(() => {
    // Create a spy object for HomeService
    homeServiceMock = jasmine.createSpyObj('HomeService', ['DoStuff']);

    TestBed.configureTestingModule({
      declarations: [AppComponent],
      providers: [{provide: HomeService, useValue: homeServiceMock}] // Provide the spy object directly
    });

    fixture = TestBed.createComponent(AppComponent);
    appComponent = fixture.componentInstance;
  });

  it('should call DoStuff() when CallHomeService() is called', () => {
    // Call the CallHomeService() method of AppComponent
    appComponent.CallHomeService();

    // Expect that DoStuff() was called on the mock HomeService
    expect(homeServiceMock.DoStuff).toHaveBeenCalled(); // Use toHaveBeenCalled() matcher
  });
});

But I get

AppComponent should call DoStuff() when CallHomeService() is called FAILED
        Expected spy HomeService.DoStuff to have been called.
            at <Jasmine>
            at UserContext.apply (src/app/app.component.spec.ts:28:37)
            at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:375:26)
            at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:287:39)
            at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:374:52)

So I tried using the TestBed.overrideProvider and it worked:

import { TestBed, ComponentFixture } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { HomeService } from './service/home.service';

describe('AppComponent', () => {
  let fixture: ComponentFixture<AppComponent>;
  let appComponent: AppComponent;
  let homeService: jasmine.SpyObj<HomeService>; // Use jasmine.SpyObj type for mock

  beforeEach(() => {
    // Create a mock object for HomeService
    const homeServiceMock = jasmine.createSpyObj('HomeService', ['DoStuff']);

    TestBed.configureTestingModule({
      declarations: [AppComponent],
      providers: []
    });

    // Override the provider for HomeService with the mock
    TestBed.overrideProvider(HomeService, { useValue: homeServiceMock });

    fixture = TestBed.createComponent(AppComponent);
    appComponent = fixture.componentInstance;
  });

  it('should call DoStuff() when CallHomeService() is called', () => {
    // Call the CallHomeService() method of AppComponent
    appComponent.CallHomeService();

    // Expect that DoStuff() was called on the mock HomeService
    expect(homeService.DoStuff).toHaveBeenCalled();
  });
});

I saw here that if I use providers in the ts component it will override my Spy, but I still do not understand why my useValue is being override. It is not supposed only to provide the mock service in providers of TestBed.configureTestingModule with useValue to call the mock service? Thanks for any help.


Solution

  • Since you're providing HomeService to your component via the component's providers array, the component is not getting the instance of HomeService created at the module level—the one you replaced with useValue—it's getting its own, separate instance.

    If you remove HomeService from AppComponent's providers array, your original setup without overrideProvider should work.

    From the docs:

    Angular DI has a hierarchical injection system, which means that nested injectors can create their own service instances. Whenever Angular creates a new instance of a component that has providers specified in @Component(), it also creates a new child injector for that instance.

    Child modules and component injectors are independent of each other, and create their own separate instances of the provided services.