angularionic-frameworkjasmine

Unit Test Angular Standalone Component, Overriding Provider not used


I have a angular component, using a class that I want to mock in the unit test. So I override this in the provider-section of my test. But the component still calls the real class and I get an error.

This worked fine in older projects without standalone components, but I think I need to do something different for standalone components? But I couldn't figure out what :-(

I simplified it to this test case:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IonicModule, ModalController } from '@ionic/angular';
import { BrowserModule } from '@angular/platform-browser';
import { Component } from '@angular/core';

// Dummy Standalone Component, just a single method that uses the ionic ModalController
@Component({
    selector: 'app-test-dialog',
    template: '',
    imports: [
        IonicModule
    ],
    standalone: true
})
export class TestDialogComponent {

    constructor(private modalCtrl: ModalController) {

    }

    async close() {
        await this.modalCtrl.dismiss();
    }

}


// Test, should override the dismiss method of the ModalController
describe('TestDialogComponent', () => {
    let component: TestDialogComponent;
    let fixture: ComponentFixture<TestDialogComponent>;

    beforeEach(async () => {
        await TestBed.configureTestingModule({
            imports: [
                BrowserModule,
                IonicModule.forRoot(),
                TestDialogComponent
            ],
            providers: [
                {
                    // provide our own ModalController
                    provide: ModalController, useValue: {
                        dismiss: () => Promise.resolve()
                    }
                }
            ]
        }).compileComponents();
        fixture = TestBed.createComponent(TestDialogComponent);
        component = fixture.componentInstance;
    });

    // this should call the provided mock class
    // but I get 'overlay does not exist thrown' 
    // because it calls the real method :-(
    it('close closes the dialog', async () => {
        const modalController = TestBed.inject(ModalController);
        const dismissSpy = spyOn(modalController, 'dismiss');
        await component.close();
        expect(dismissSpy).toHaveBeenCalled();
    });

});


Solution

  • I found the following solution. But I don't get why I need this in this case and in other cases the overridden providers work...

    import { ComponentFixture, TestBed } from '@angular/core/testing';
    import { IonicModule, ModalController } from '@ionic/angular';
    import { BrowserModule } from '@angular/platform-browser';
    import { Component } from '@angular/core';
    
    @Component({
        selector: 'app-test-dialog',
        template: '',
        imports: [
            IonicModule
        ],
        standalone: true
    })
    export class TestDialogComponent {
    
        constructor(private modalCtrl: ModalController) {
    
        }
    
        async close() {
            await this.modalCtrl.dismiss();
        }
    
    }
    
    // use a mock-class so we can directly spy on it
    class ModalControllerMock {
        dismiss() {
            return Promise.resolve();
        }
    }
    
    describe('TestDialogComponent', () => {
        let component: TestDialogComponent;
        let fixture: ComponentFixture<TestDialogComponent>;
        const modalControllerMock = new ModalControllerMock();
    
        beforeEach(async () => {
            await TestBed.configureTestingModule({
                imports: [
                    BrowserModule,
                    IonicModule.forRoot(),
                    TestDialogComponent
                ]
            }).compileComponents();
            // use this provider instead of the provider in configureTestingModule
            TestBed.overrideComponent(TestDialogComponent, {
                set: {
                    providers: [{  provide: ModalController, useValue: modalControllerMock}],
                },
            });
            fixture = TestBed.createComponent(TestDialogComponent);
            component = fixture.componentInstance;
        });
    
        it('close closes the dialog', async () => {
            const dismissSpy = spyOn(modalControllerMock, 'dismiss');
            await component.close();
            expect(dismissSpy).toHaveBeenCalled();
        });
    
    });