Search code examples
javascriptangularunit-testingjestjsjasmine

Jest mock is not properly used/replaced in Angular standalone component unit test


I have one simple Ionic/Angular component. I reduced other unnecessary code.

import { IonicModule, ModalController } from '@ionic/angular';

@Component({
  selector: 'my-component',
  templateUrl: 'my-component.html',
  styleUrls: ['my-component.scss'],
  standalone: true,
  imports: [
    IonicModule
  ]
})

export class MyComponent implements OnInit {
  @Input() inputParam: Organization;

  public paramOne: string;

  constructor(private modalCtrl: ModalController) {}

  ngOnInit(): void {
    this.paramOne = this.inputParam?.id;
  }

  onClickClose(): void {
    this.modalCtrl.dismiss();
  }
}

My test is also extremely simple. I am using jest:

import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ModalController } from '@ionic/angular';
import { MyComponent } from './my.component';

describe('MyComponent', () => {
  let component: MyComponent;
  let fixture: ComponentFixture<MyComponent>;
  let ionicModalControllerMock = {
     dismiss: jest.fn()
  };

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [MyComponent, NoopAnimationsModule],
      providers: [{ provide: ModalController, useValue: ionicModalControllerMock }]
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
  });

   it('should dismiss the modal without data on close click', () => {
     component.onClickClose();
     expect(ionicModalControllerMock.dismiss).toHaveBeenCalledWith();
   });
});

My problem is that my ionicModalControllerMock is not being used correctly. My test fails with the error that the dismiss method was never called. In the debugger I see that my mock is not being used. And in many other places/tests this kind of mock works for me. Is there something I missed here?


Solution

  • After further research and experimentation, I discovered the root cause of the issue related to the usage of ModalController mock in testing an Angular standalone component. The issue was that when using a module that contains ModalController (like IonicModule), it can override the mock configuration set up in your tests.

    To resolve this issue, you can use the overrideProvider method provided by Angular's TestBed. This method allows you to precisely define or override providers after the module configuration in your test setup. Here's how you can implement it:

    import { NoopAnimationsModule } from '@angular/platform-browser/animations';
    import { ComponentFixture, TestBed } from '@angular/core/testing';
    import { ModalController } from '@ionic/angular';
    import { MyComponent } from './my.component';
    import { IonicModule } from '@ionic/angular';
    
    describe('MyComponent', () => {
      let component: MyComponent;
      let fixture: ComponentFixture<MyComponent>;
      let ionicModalControllerMock = {
        dismiss: jest.fn()
      };
    
      beforeEach(async () => {
        await TestBed.configureTestingModule({
          imports: [MyComponent, NoopAnimationsModule, IonicModule],
        }).compileComponents();
    
        TestBed.overrideProvider(ModalController, { useValue: ionicModalControllerMock });    
      });
    
      beforeEach(async () => {
        fixture = TestBed.createComponent(MyComponent);
        component = fixture.componentInstance;
      });
    
      it('should dismiss the modal without data on close click', () => {
        component.onClickClose();
        expect(ionicModalControllerMock.dismiss).toHaveBeenCalled();
      });
    });
    

    Using overrideProvider ensures that your mock for ModalController is used instead of the actual implementation, even after importing IonicModule in your test. This is particularly useful in scenarios where modules automatically register their providers, which can lead to your mocks being overridden.

    Here is another example showing how overrideProvider can be used directly within the TestBed.configureTestingModule method for a different scenario:

     await TestBed.configureTestingModule({
      imports: [MyComponent, NoopAnimationsModule, IonicModule],
    })
     .overrideProvider(ModalController, {
         useValue: ionicModalControllerMock
      })
     .compileComponents()
      
    

    This example further illustrates the versatility of overrideProvider in ensuring that the correct mocks are used, maintaining the integrity of your tests.

    I hope this solution will be helpful to others facing similar issues in testing Angular standalone components.