Search code examples
angularunit-testingjestjs

Mock provided service in Standalone component in Jest unit test


I have a service that is not provided in root, instead it is provided in my component like so:

@Injectable()
export class NavbarFacadeService extends AbstractStateFacade<IDrawerState, DrawerStateActions> implements OnDestroy {
   ...
}
@Component({
  selector: 'navbar-drawer',
  standalone: true,
  imports: [],
  providers: [NavbarFacadeService],
  template: `
     ...
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NavbarDrawerComponent implements AfterViewInit {
   ...
}

The reason for providing it in the component vs making it provided in root, is because I want the service to be cleaned up automatically when the component is destroyed. This is working fine. The issue is unit testing this.

In my unit test, i should be able to mock the instance of the NavbarFacadeService like so:

const mockNavbarFacadeService = MockService(NavbarFacadeService, {
  ...
});

 beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [NavbarDrawerComponent],
      providers: [
        { provide: NavbarFacadeService, useValue: mockNavbarFacadeService },
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(NavbarDrawerComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

However, when provided in this way, when the unit tests run, the service is not of a mocked instance but instead the actual provided instance and is executing real functions instead of the ones i mocked.

Yet, if I remove the provider from the component, and go back to making the service provided in root then it correctly gets the mocked instance.

So, when working with standalone components that provide a service, is there some other way of setting up the TestBed to provide the mocked value that I am just not aware of?


Solution

  • Angular will create a global shared single instance of a servie declared with @Injectable({providedIn: 'root'}), on the other hand, a service declared without @Injectable({providedIn: 'root'}) will have seperated instances on each component use this service.

    When you remove {providedIn: 'root'} from your service, you must provide it in your components as below or your injection in constructor won't work.

    @Component({
      selector: 'navbar-drawer',
      providers: [NavbarFacadeService], // <-- here
    })
    export class NavbarDrawerComponent implements OnInit {
    }
    

    But, when you run test cases, the mocked service won't work, because the component has its own injected servcie, When TestBed create the component, Angualr will create a new instance of this service which is different with the one in your test case, and the component will use this instance when running test case.

    beforeEach(() => {
      fixture = TestBed.createComponent(NavbarDrawerComponent); // <-- Here
      component = fixture.componentInstance;
      fixture.detectChanges();
    });
    

    There are two ways to fix this issue

    1. Use overrideProvider
    beforeEach(async () => {
      await TestBed.configureTestingModule({
        imports: [NavbarDrawerComponent],
      }).compileComponents();
      TestBed.overrideProvider(NavbarFacadeService, {useValue: mockNavbarFacadeService}); // <--
    });
    
    1. Use overrideComponent
    beforeEach(async () => {
      await TestBed.configureTestingModule({
        imports: [NavbarDrawerComponent],
      })
        .overrideComponent(NavbarDrawerComponent, {
          set: {
            providers: [
              {provide: NavbarFacadeService, useValue: mockNavbarFacadeService},
            ]
          }
        }).compileComponents();
    });