MatDialog服务单元测试Angular 6错误

43
我正在使用模态服务来打开、确认和关闭对话框,我正在编写它的单元测试文件,但在 Angular 上出现了错误,以下是代码。

modal.service.ts

@Injectable()
export class ModalService {

  constructor(private dialog: MatDialog) { }

  public open<modalType>(modalComponent: ComponentType<modalType>): Observable<any> {
    let dialogRef: MatDialogRef<any>;

    dialogRef = this.dialog.open(modalComponent, {
      maxWidth: '100vw'
    });
    console.log(dialogRef)
    dialogRef.componentInstance.body = body;

    return dialogRef.afterClosed().pipe(map(result => console.log('test'); );
  }

}

modal.service.spec.ts

export class TestComponent  {}


describe('ModalService', () => {
  let modalService: ModalService;

  const mockDialogRef = {
    open: jasmine.createSpy('open')
  };

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [ MatDialogModule ],
      providers: [
        ModalService,
        MatDialogRef,
        { provide: MatDialog, useClass: MatDialogStub }
      ]
    }).compileComponents();

    modalService = TestBed.get(ModalService);
  }));


  it('open modal', () => {
    modalService.open(DummyComponent, '300px');
    expect(modalService.open).toHaveBeenCalled();

  });

});

所以,使用那段代码时出现了错误。
TypeError: Cannot read property 'componentInstance' of undefined

你能帮我如何让这个成功吗?非常感谢帮助。

请查看这个 mat dialog 的示例,确保已经导入了所有必需的模块 https://stackblitz.com/angular/gxyboyyobmo - Daniel C.
@DanielC. 嘿,谢谢你的建议,但我正在寻找单元测试答案。该服务在组件中被调用时运行良好,但在单元测试中却不行。 - rj.learn
6个回答

61

测试mat-dialogs可能有些棘手。 我倾向于使用一个spy对象来返回对话框打开的结果(在下面的dialogRefSpyObj中),这样我可以更轻松地跟踪和控制测试。 在您的情况下,它可能看起来像以下内容:

describe('ModalService', () => {
    let modalService: ModalService;
    let dialogSpy: jasmine.Spy;
    let dialogRefSpyObj = jasmine.createSpyObj({ afterClosed : of({}), close: null });
    dialogRefSpyObj.componentInstance = { body: '' }; // attach componentInstance to the spy object...

    beforeEach(() => {
        TestBed.configureTestingModule({
            imports: [MatDialogModule],
            providers: [ModalService]
        });
        modalService = TestBed.get(ModalService);
    });

    beforeEach(() => {
        dialogSpy = spyOn(TestBed.get(MatDialog), 'open').and.returnValue(dialogRefSpyObj);
    });

    it('open modal ', () => {
        modalService.open(TestComponent, '300px');
        expect(dialogSpy).toHaveBeenCalled();

        // You can also do things with this like:
        expect(dialogSpy).toHaveBeenCalledWith(TestComponent, { maxWidth: '100vw' });

        // and ...
        expect(dialogRefSpyObj.afterClosed).toHaveBeenCalled();
    });
});

你如何触发 dialogRefSpyObj.afterClosed - PinguinoSod
你如何测试一个由另一个模态框打开的模态框?(应用程序>单击按钮>第一个基本确认模态框>单击“是”>在新模态框中打开组件) - user3659739
我会独立测试这两个模态框(每个都有自己的一组测试)。 - dmcgrandle
是的,但问题在于当我在MatDialog上放置间谍时,它总是返回我打开的第一个对话框,所以我不知道如何监视由第一个对话框打开的第二个对话框。 - user3659739
如果你正在进行单元测试,那么不要将第二个测试与第一个测试一起测试,而是将它们分别隔离测试。这样,你只会测试到“第一个”对话框。如果你正在进行集成测试或端到端测试,你需要使用其他工具,如cypress.io。这个问题是关于单元测试的。如果你需要更多帮助,我建议你提出另一个StackOverflow问题,而不是继续聊天对话。 - dmcgrandle
显示剩余3条评论

15

我有一个更好的解决方案,仍可在2019年使用

header.component.ts

import { BeforeLogOutComponent } from '@app-global/components/before-log-out/before-log-out.component';


  /**
   * The method triggers before the logout.
   * Opens the dialog and warns the user before log Out.
   */
  public beforeLogOut(): void {
    this._dialog.open(BeforeLogOutComponent, { width: '400px', disableClose: false, panelClass: 'dialog_before_log_out' })
    .afterClosed()
    .subscribe((res) => {
      if (res && res.action === true) { this.loggedOut(); }
    }, err => {
      console.error(err);
    });
  }

header.component.spec.ts

-->

header.component.spec.ts

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MatDialog } from '@angular/material';
import { Observable, of } from 'rxjs';



<<-- Create a MatDialog mock class -->>
export class MatDialogMock {
  // When the component calls this.dialog.open(...) we'll return an object
  // with an afterClosed method that allows to subscribe to the dialog result observable.
  open() {
    return {
      afterClosed: () => of({action: true})
    };
  }
}



describe('HeaderComponent', () => {

  let component: HeaderComponent;
  let fixture: ComponentFixture<HeaderComponent>;

  beforeEach(async(() => {

    TestBed.configureTestingModule({
      imports: [
        MaterialModule, RouterTestingModule, HttpModule, BrowserAnimationsModule,
        HttpClientModule, FlexLayoutModule,
      ],
      declarations: [
        HeaderComponent,
      ],
      providers: [
        { provide: MatDialog, useClass: MatDialogMock } <<-- look this
      ]
    })
    .compileComponents();
  }));



  beforeEach(async() => {
    fixture = TestBed.createComponent(HeaderComponent);
    component = fixture.componentInstance;


    component.ngOnInit();
    component.ngAfterViewInit();
    await fixture.whenStable();
    fixture.detectChanges();
  });


  fit('should create', () => {
    expect(component).toBeTruthy();
  });


  // I test the dialog here.
  fit('should open the dialog', () => {
    component.beforeLogOut();
  });


}

6
实际上,我在之前的回答中提供的间谍物件解决方案在2019年的Angular 7中完全可行。 :) 我更喜欢这个方法而不是你提供的模拟类方法,虽然它们都能解决问题,但只是一个偏好的问题,可以选择哪种方法。 - dmcgrandle
3
不确定如何测试 afterClosed,因为在规范文件中没有任何指示对话框关闭的内容被调用。 - ChumiestBucket
1
请考虑解释您的代码如何帮助解决提问者的问题,而不是简单地给出代码。 - Edric
1
谢谢,这在我的情况下起作用了,因为模拟整个对话响应解决了问题,并按要求返回了true/false。 - Ashwin

7

我没有针对你的情况确切的答案,但我也测试了MatDialog。我可以向您展示我所做的内容。也许可以看一下inject()部分:

(为了清晰和保密性,我删除了一些内容)

describe('MyDialogComponent', () => {
  let dialog: MatDialog;
  let overlayContainer: OverlayContainer;
  let component: MyDialogComponent;
  let fixture: ComponentFixture<MyDialogComponent>;
  const mockDialogRef = {
    close: jasmine.createSpy('close')
  };

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        BrowserAnimationsModule,
        ReactiveFormsModule,
        AngularMaterialModule,
      ],
      providers: [
        { provide: MatDialogRef, useValue: mockDialogRef },
        {
          provide: MAT_DIALOG_DATA,
          useValue: {
            title: 'myTitle',
          }
        }
      ],
      declarations: [MyDialogComponent],
    });

    TestBed.overrideModule(BrowserDynamicTestingModule, {
      set: {
        entryComponents: [MyDialogComponent]
      }
    });

    TestBed.compileComponents();
  }));

  beforeEach(inject([MatDialog, OverlayContainer],
    (d: MatDialog, oc: OverlayContainer) => {
      dialog = d;
      overlayContainer = oc;
    })
  );

  afterEach(() => {
    overlayContainer.ngOnDestroy();
  });

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

  it('should create', () => {
    expect(component).toBeTruthy();
  });


  it('onCancel should close the dialog', () => {
    component.onCancel();
    expect(mockDialogRef.close).toHaveBeenCalled();
  });

});

2
将此部分添加到提供商部分。
{ provide: MAT_DIALOG_DATA, useValue: {} },
{ provide: MatDialogRef, useValue: {} },

请查看以下内容

describe('MyDialogComponent', () => {
   let component: MyDialogComponent;
   let fixture: ComponentFixture<MyDialogComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
       imports: [
          MatDialogModule,
       ],
       declarations: [MyDialogComponent],
       providers: [
          { provide: MAT_DIALOG_DATA, useValue: {} },
          { provide: MatDialogRef, useValue: {} },
       ],
    }).compileComponents();
   }));
 });

0

这个答案并没有直接回答问题,但是对于像我一样因为无法查询到打开的对话框而到这里的人来说会有帮助。

使用 spectator 时,您只需将 { root: true } 作为查询选项包含即可。

对话框之所以找不到,是因为它不是正在测试的组件的子级,但使用 { root: true } 将搜索整个页面。

例如:spectator.query(byTestId('myTestId'), { root: true })


0
it('应该打开一个对话框', () => {
    //arrange
    const dialog = TestBed.inject(MatDialog); 

    //act 
    spyOn(dialog,'open').and.callThrough();
    component.openDialog();
    
    //assert
    expect(dialog.open).toHaveBeenCalled();
})

欢迎来到Stack Overflow!感谢您的回答。请提供更多关于您解决方案的细节。代码片段、高质量的描述或任何相关信息都会很好。对于每个人来说,清晰简洁的答案更有帮助,也更容易理解。请通过编辑您的回答,提供具体细节以提高您的回答质量。欲了解更多信息,请参阅如何:撰写好的答案。祝您编码愉快! - undefined
就目前的写法来看,你的回答不够清晰。请编辑以添加更多细节,以帮助其他人理解这如何回答所提出的问题。你可以在帮助中心找到关于如何撰写好回答的更多信息。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接