rmcsharry
rmcsharry

Reputation: 5562

Angular Jest Testing a component that opens a MatDialog - open is not a function

Similar to this question, but it doesn't provide an answer that works for me.

I have a simple component that has a method that opens a dialog:

  enterGiveaway() {
    this.dialog.open(SpendTicketsDialogComponent, {
      width: '370px',
      height: '600px'
    });
  }

For now I just want to test that calling that method results in the dialog being opened.

The test fails with this error:

  expect(spy).toBeCalledTimes(expected)

    Expected number of calls: 1
    Received number of calls: 0

with this code:

    import {async, ComponentFixture, TestBed} from '@angular/core/testing';
    
    import {GiveawayItemComponent} from './giveaway-item.component';
    import {giveawaysMock} from '../../../../../mocks/giveaways.mock';
    import {MaterialModule} from '../../material.module';
    import {getTranslocoModule} from '../../../transloco-testing.module';
    import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
    import {MatDialog} from '@angular/material/dialog';
    import {EMPTY} from 'rxjs';
    import {SpendTicketsDialogComponent} from '../dialogs/tickets-dialog/spend-tickets-dialog.component';
    import {NumberFormatter} from '../../filters/numberFormatter/numberFormatter.filter';
    import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
    import {BrowserModule} from '@angular/platform-browser';
    
    describe('GiveawayItemComponent', () => {
      let component: GiveawayItemComponent;
      let fixture: ComponentFixture<GiveawayItemComponent>;
      let dialog: any;
    
      beforeEach(async(() => {
        TestBed.configureTestingModule({
          declarations: [
            GiveawayItemComponent,
            SpendTicketsDialogComponent,
            NumberFormatter
          ],
          imports: [
            MaterialModule,
            BrowserAnimationsModule,
            getTranslocoModule({})
          ],
          schemas: [CUSTOM_ELEMENTS_SCHEMA]
        })
          .overrideModule(BrowserModule, {
            set: {entryComponents: [SpendTicketsDialogComponent]}
          })
          .compileComponents();
      }));
    
      beforeEach(() => {
        fixture = TestBed.createComponent(GiveawayItemComponent);
        component = fixture.componentInstance;
        component.giveaway = giveawaysMock[0];
        component.numberOfChances = 100;
        dialog = TestBed.inject(MatDialog);
        fixture.detectChanges();
      });
    
      it('should create', () => {
        expect(component).toBeTruthy();
      });
    
      describe('enterGiveaway', () => {
        it('should open the spend tickets dialog', async(() => {
          component.enterGiveaway();
          fixture.detectChanges();
          const spy = spyOn(dialog, 'open').and.returnValue({
            afterClosed: () => EMPTY
          });
    
          expect(spy).toBeCalledTimes(1);
        }));
      });
    });

I understand of course, that MatDialog is not referencing the actual SpendTicketsDialogComponent which is the one that is opened. So I tried providing a mock object for the dialog:

    import {async, ComponentFixture, TestBed} from '@angular/core/testing';
    
    import {GiveawayItemComponent} from './giveaway-item.component';
    import {giveawaysMock} from '../../../../../mocks/giveaways.mock';
    import {MaterialModule} from '../../material.module';
    import {getTranslocoModule} from '../../../transloco-testing.module';
    import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
    import {MatDialog} from '@angular/material/dialog';
    import {of} from 'rxjs';
    import {SpendTicketsDialogComponent} from '../dialogs/tickets-dialog/spend-tickets-dialog.component';
    import {NumberFormatter} from '../../filters/numberFormatter/numberFormatter.filter';
    import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
    import {BrowserModule} from '@angular/platform-browser';
    
    class dialogMock {
      open() {
        return {
          afterClosed: () => of({})
        };
      }
    }
    
    describe('GiveawayItemComponent', () => {
      let component: GiveawayItemComponent;
      let fixture: ComponentFixture<GiveawayItemComponent>;
      let dialog: any;
    
      beforeEach(async(() => {
        TestBed.configureTestingModule({
          declarations: [
            GiveawayItemComponent,
            SpendTicketsDialogComponent,
            NumberFormatter
          ],
          imports: [
            MaterialModule,
            BrowserAnimationsModule,
            getTranslocoModule({})
          ],
          providers: [{provide: MatDialog, useValue: dialogMock}],
          schemas: [CUSTOM_ELEMENTS_SCHEMA]
        })
          .overrideModule(BrowserModule, {
            set: {entryComponents: [SpendTicketsDialogComponent]}
          })
          .compileComponents();
      }));
    
      beforeEach(() => {
        fixture = TestBed.createComponent(GiveawayItemComponent);
        component = fixture.componentInstance;
        component.giveaway = giveawaysMock[0];
        component.numberOfChances = 100;
        dialog = TestBed.inject(MatDialog);
        fixture.detectChanges();
      });
    
      it('should create', () => {
        expect(component).toBeTruthy();
      });
    
      describe('enterGiveaway', () => {
        it('should open the spend tickets dialog', async(() => {
          component.enterGiveaway();
          fixture.detectChanges();
          const spy = spyOn(dialog, 'open').and.callThrough();
    
          expect(spy).toBeCalledTimes(1);
        }));
      });
    });

but this throws the error this.dialog.open is not a function.

I actually don't think either solution is correct, because I need to check that calling enterGiveaway opens the SpendTicketsDialog.

So how can I verify that?

Upvotes: 5

Views: 13212

Answers (2)

yurzui
yurzui

Reputation: 214175

Your mock for MatDialog is not good enough.

providers: [{provide: MatDialog, useValue: dialogMock}],

Since dialogMock is a class then you should either use it like:

useValue: new dialogMock()

or

useClass: dialogMock

Otherwise your mock won't have open method.

Tip: always name your classes with Capital letter

Now let's go to your test case and pay attention on the execution order:

component.enterGiveaway();   <-------------------------  (1)         
...
const spy = spyOn(dialog, 'open').and.callThrough(); <-- (2)

expect(spy).toBeCalledTimes(1); <----------------------- (3)
  • (1) is where modal.open() method is being executed. If we provided corrected mock for dialog then it will be executed without any problems

  • (2) is where you spy on dialog method in order to count call times. This spy won't be called later. modal.open() was already executed at step (1)

  • (3) your test fails since there wasn't any calles for your spy for the reasons described in (2) step.

The solution is quite self-explanatory: put spy procedure before executing enterGiveaway() method:

  const spy = spyOn(dialog, 'open').and.callThrough();
  component.enterGiveaway();

jasmine.createSpyObj

There is another handy way of how to mock your MatDialog and open method by using jasmine.createSpyObj.

let dialog: jasmine.SpyObj<MatDialog>;
...

{provide: MatDialog, useValue: jasmine.createSpyObj<MatDialog>(['open'])}

beforeEach(() => {
  ...
  dialog = TestBed.inject(MatDialog) as jasmine.SpyObj<MatDialog>;
  ...
});

it('should open the spend tickets dialog', async(() => {
  component.enterGiveaway();

  expect(dialog.open.calls.count()).toBe(1);
}));

Upvotes: 7

Vivek Jain
Vivek Jain

Reputation: 2864

You can try below code. Simply define dialog.open = jest.fn(); after that call component.enterGiveaway(); then verfiy expect(dialog.open).toBeCalledTimes(1);.

describe('enterGiveaway', () => {
  it('should open the spend tickets dialog', async(() => {
    // setup
    dialog.open = jest.fn();

    // execute
    component.enterGiveaway();

    // verify
    expect(dialog.open).toBeCalledTimes(1);
  }));
});

Upvotes: 2

Related Questions