You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular单元测试报错:Cannot set property 'componentInstance' of undefined

问题描述

尝试了多种方案(构造函数注入组件、以不同方式创建mock和spy、将spy注入元素等),但始终遇到相同错误:Cannot set property 'componentInstance' of undefined,恳请提供解决思路。

组件代码

openUpdate(rowSelected: iData) {
        let dialogRef = this.dialog.open(DialogEditComponent, {});
        dialogRef.componentInstance.dialogRef = dialogRef;
        dialogRef.componentInstance.selectedData = rowSelected;
        const sub = dialogRef.componentInstance.onAdd.subscribe((data: iData) => {
            if (data) {
                this.update(data);
            }
        });
        dialogRef.afterClosed().subscribe(() => {
            sub.unsubscribe();
        });
    }

测试代码(SPEC.TS)

describe('Component1Component', () => {
  let component: Componenet1Component;
  let fixture: ComponentFixture<Componenet1Component>;
  let dataServiceSpy: jasmine.SpyObj<DataService>;
  let toastrServiceSpy: jasmine.SpyObj<ToastrService>;
  let dialogSpy: any;
  let dialogRefSpy: any;

  beforeEach(async () => {
    dialogSpy = {
      open: jasmine.createSpy('open').and.returnValue({
        componentInstance: {
          onAdd: jasmine.createSpyObj('onAdd', ['subscribe'])
        },
        afterClosed: () => {
          return jasmine.createSpyObj('afterClosed', ['subscribe']);
        },
      })
    };
    toastrServiceSpy = jasmine.createSpyObj('ToastrService', ['success', 'error']);

    await TestBed.configureTestingModule({
      declarations: [Component1Component, DialogEditComponent],
      imports: [MatDialogModule],
      providers: [
        { provide: MatDialog, useValue: dialogSpy },
        { provide: MatDialogRef, useValue: dialogRefSpy },
        { provide: FinalistService, useValue: dataServiceSpy },
        { provide: ToastrService, useValue: toastrServiceSpy },
        { provide: MAT_DIALOG_DATA, useValue: {} },
      ],
    }).compileComponents();
  });

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

it('should open the edit dialog with the selected data', async () => {
    component = new Component1Component(dialogSpy, dataServiceSpy, toastrServiceSpy);

    dialogRefSpy.componentInstance = {
      dialogRef: dialogRefSpy,
      selectedFinalist: mockData[0],
      onAdd: of(mockData[0])
    };

    dialogSpy.open.and.returnValue(dialogRefSpy);

    spyOn(component, 'update');

    component.openUpdate(mockData[0]);

    expect(dialogSpy.open).toHaveBeenCalledWith(DialogEditComponent, {});
    expect(dialogRefSpy.componentInstance.selectedData).toEqual(mockData[0]);

    expect(component.update).toHaveBeenCalledWith(mockData[0]);
    expect(dialogRefSpy.afterClosed).toHaveBeenCalled();
  });

报错信息

TypeError: Cannot set property 'componentInstance' of undefined
            at UserContext.<anonymous> (src/app/features/Component1.component.spec.ts:108:35)
            at Generator.next (<anonymous>)
            at asyncGeneratorStep (node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js:3:1)
            at _next (node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js:22:1)
            at executor (node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js:27:1)
            at new ZoneAwarePromise (node_modules/zone.js/dist/zone.js:1351:25)
            at UserContext.apply (node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js:19:1)
            at _ZoneDelegate.invoke (node_modules/zone.js/dist/zone.js:409:30)
            at ProxyZoneSpec.onInvoke (node_modules/zone.js/dist/zone-testing.js:303:43)
            at _ZoneDelegate.invoke (node_modules/zone.js/dist/zone.js:408:56)
解决思路
  • 先初始化dialogRefSpy
    报错核心原因是dialogRefSpy未赋值就直接设置componentInstance属性。在测试用例或beforeEach里先给它初始化一个基础结构:

    dialogRefSpy = {
      componentInstance: {},
      afterClosed: jasmine.createSpy('afterClosed').and.returnValue(of(null))
    };
    
  • 统一mock结构,避免重复定义
    原本beforeEach里已经给dialogSpy.open返回了一个mock的dialogRef,测试用例里又重新替换成dialogRefSpy,导致结构混乱。建议在beforeEach里统一初始化dialogRefSpy,并让dialogSpy.open直接返回它:

    beforeEach(async () => {
      // 先初始化完整的dialogRefSpy
      dialogRefSpy = {
        componentInstance: {
          onAdd: new Subject(), // 用Subject方便后续触发订阅回调
          dialogRef: null,
          selectedData: null
        },
        afterClosed: jasmine.createSpy('afterClosed').and.returnValue(of(null))
      };
      dialogRefSpy.componentInstance.dialogRef = dialogRefSpy;
    
      dialogSpy = {
        open: jasmine.createSpy('open').and.returnValue(dialogRefSpy)
      };
      // ...其他初始化代码
    });
    
  • 修复组件实例创建方式
    测试用例里直接new Component1Component会绕开TestBed的依赖注入,建议直接用fixture.componentInstance;另外dataServiceSpy也没初始化,要提前创建:

    // 在beforeEach里初始化dataServiceSpy
    dataServiceSpy = jasmine.createSpyObj('DataService', ['update']);
    
  • 正确触发onAdd事件验证逻辑
    如果用Subject模拟onAdd,调用openUpdate后手动触发next,才能验证update是否被正确调用:

    component.openUpdate(mockData[0]);
    dialogRefSpy.componentInstance.onAdd.next(mockData[0]); // 触发订阅回调
    expect(component.update).toHaveBeenCalledWith(mockData[0]);
    
  • 移除不必要的依赖
    测试Component1Component时,不需要提供MatDialogRef和MAT_DIALOG_DATA,这两个是给弹窗组件DialogEditComponent用的,删掉这两个provider避免干扰。

内容的提问来源于stack exchange,提问作者Kenzo_Gilead

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 10:27:08