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

Angular6动态组件单元测试报错:无法读取subscribe属性

解决Angular动态组件单元测试中subscribe undefined错误

你遇到的问题核心很明确:你模拟的动态组件MockRefreshComponent没有实现CompComponent接口要求的updateEmitter和closeEmitter输出属性。当测试中动态创建这个组件实例后,代码尝试访问这两个未定义的属性并调用subscribe,自然会抛出TypeError。

下面是具体的修复步骤和优化后的测试代码:

步骤1:完善模拟组件的接口实现

首先,修改MockRefreshComponent,添加CompComponent接口要求的data输入属性,以及updateEmitter、closeEmitter输出属性:

import { Component, Input, Output, EventEmitter } from '@angular/core';
// 导入你的CompComponent接口
import { CompComponent } from '../../../core/pages/servicecatalog/models/comp.compoent.model';

@Component({
 selector: 'product-settings',
 template: '<p>Mock Product Settings Component</p>'
})
class MockRefreshComponent implements CompComponent {
 @Input() data: any;
 @Output() updateEmitter = new EventEmitter<any>();
 @Output() closeEmitter = new EventEmitter<any>();
}

这样,当动态创建这个组件实例时,updateEmitter和closeEmitter就不再是undefined,subscribe调用就能正常执行了。

步骤2:优化测试用例,验证Emitter的订阅行为

接下来,我们可以添加测试用例,验证当动态组件的emitter触发时,宿主组件的对应逻辑是否正常执行:

测试updateEmitter的行为

it('should call dialog.close when updateEmitter emits', () => {
  // 确保动态组件已加载完成
  const dynamicInstance = component.componentRef.instance as MockRefreshComponent;
  const testResponse = { orderNumber: 'TEST-123' };

  // 触发updateEmitter
  dynamicInstance.updateEmitter.emit(testResponse);

  // 验证dialog.close是否被正确调用
  expect(MatDialogRefMock.close).toHaveBeenCalledWith({
    confirm: true,
    orderid: testResponse.orderNumber,
    orderNumber: testResponse.orderNumber
  });
});

测试closeEmitter的行为

it('should call closeDialog when closeEmitter emits', () => {
  spyOn(component, 'closeDialog');
  const dynamicInstance = component.componentRef.instance as MockRefreshComponent;
  
  // 触发closeEmitter
  dynamicInstance.closeEmitter.emit();

  expect(component.closeDialog).toHaveBeenCalledWith('close');
});

步骤3:确保ViewChild和动态组件创建逻辑正确

在测试初始化阶段,我们需要确保组件的异步数据加载和动态组件创建逻辑完成:

beforeEach(() => {
  fixture = TestBed.createComponent(DynamicComponentHolderComponent);
  component = fixture.componentInstance;
  // 第一次变更检测初始化ViewChild
  fixture.detectChanges();
  // 触发组件数据加载
  component.getCustomComponentList();
  // 第二次变更检测执行动态组件加载逻辑
  fixture.detectChanges();
});

完整修改后的测试代码片段

这里整合了所有修复后的关键部分:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DynamicComponentHolderComponent } from './dynamic-component-holder.component';
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { CompService } from '../../services/comp.service';
import { NO_ERRORS_SCHEMA, Component, Input, Output, EventEmitter } from '@angular/core';
import { of } from 'rxjs';
import { CompItem } from '../../../core/pages/servicecatalog/models/comp.item.model';
import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
import { CompDirective } from '../../directive/comp-directive.directive';
import { CompComponent } from '../../../core/pages/servicecatalog/models/comp.compoent.model';

@Component({
 selector: 'product-settings',
 template: '<p>Mock Product Settings Component</p>'
})
class MockRefreshComponent implements CompComponent {
 @Input() data: any;
 @Output() updateEmitter = new EventEmitter<any>();
 @Output() closeEmitter = new EventEmitter<any>();
}

describe('DynamicComponentHolderComponent', () => {
 let component: DynamicComponentHolderComponent;
 let fixture: ComponentFixture<DynamicComponentHolderComponent>;
 const MatDialogRefMock = { close: jasmine.createSpy('close') };

 const passedData = {"data":{"categoryIds":[{"parentId":"5c3f08a7dc57c0002e3e8506","status":"Active","_id":"5c3f08b5dc57c0002e3e8507","name":"userCat-2","description":"userCat-2","createdAt":"2019-01-16T10:34:29.280Z","updatedAt":"2019-01-16T10:34:29.280Z","__v":0}],"tags":["refresh"],"status":"Active","orderFormKey":[],"headers":[],"userGroup":[{"id":"377c866e-7b43-4c5f-87fd-461c296412df","group":"service-catalog","description":"service catalog group","isAdmin":false,"created_at":"2018-12-27T04:56:57.493Z"}],"workflow":[],"_id":"5c4662df0042bc008aa1acb7","name":"Refresh Cisco Ip Phone","description":"Refresh Cisco Ip Phone","orderFormType":"customComponent","customComponent":"Refresh Cisco IP Phone","createdAt":"2019-01-22T00:25:03.626Z","updatedAt":"2019-01-22T00:25:03.626Z","__v":0,"favourite":true,"favouriteId":"5c46b56b11d3e50064cc41f5"}};

 beforeEach(async(() => {
 TestBed.configureTestingModule({
 declarations: [
 DynamicComponentHolderComponent,
 MockRefreshComponent,
 CompDirective
 ],
 imports: [MatDialogModule],
 providers: [
 { provide: MAT_DIALOG_DATA, useValue: passedData },
 { provide: MatDialogRef, useValue: MatDialogRefMock },
 { provide: CompService, useClass: MockComponentService}
 ],
 schemas: [NO_ERRORS_SCHEMA]
 });
 TestBed.overrideModule(BrowserDynamicTestingModule, {
 set: { entryComponents: [MockRefreshComponent] }
 })
 .compileComponents();
 }));

 beforeEach(() => {
 fixture = TestBed.createComponent(DynamicComponentHolderComponent);
 component = fixture.componentInstance;
 fixture.detectChanges();
 // 确保组件数据加载完成
 component.getCustomComponentList();
 fixture.detectChanges();
 });

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

 it('will have the `ViewChild`s defined', () => {
 expect(fixture.componentInstance.compHost).toBeDefined();
 });

 it('expect close Dialog to be called', () => {
 const closeDialog = spyOn(component, 'closeDialog').and.callThrough();
 component.closeDialog('close');
 expect(closeDialog).toHaveBeenCalled();
 });

 it('should call dialog.close when updateEmitter emits', () => {
 const dynamicInstance = component.componentRef.instance as MockRefreshComponent;
 const testOrderNumber = 'ORD-001';
 dynamicInstance.updateEmitter.emit({ orderNumber: testOrderNumber });

 expect(MatDialogRefMock.close).toHaveBeenCalledWith({
 confirm: true,
 orderid: testOrderNumber,
 orderNumber: testOrderNumber
 });
 });

 it('should trigger closeDialog when closeEmitter emits', () => {
 spyOn(component, 'closeDialog');
 const dynamicInstance = component.componentRef.instance as MockRefreshComponent;
 dynamicInstance.closeEmitter.emit();

 expect(component.closeDialog).toHaveBeenCalledWith('close');
 });
});

class MockComponentService {
 getComponents() {
 return of([
 new CompItem(MockRefreshComponent, {
 'displayName': 'Refresh Cisco IP Phone',
 'name': 'refreshCiscoIpPhone',
 'componentName': 'refresh-cisco',
 'classname': 'RefreshCiscoComponent',
 'width': '90%',
 'height': '95vh',
 'maxWidth': '95vw',
 'maxHeight': '95vh',
 })
 ]);
 }
}

额外注意点

  • 确保CompComponent接口确实定义了data、updateEmitter和closeEmitter,否则模拟组件的实现会不匹配。
  • 你当前closeDialog方法中有个小bug:(<CompComponent>this.componentRef).destroy()应该改为this.componentRef.destroy(),因为componentRef本身是组件引用对象,直接调用其destroy方法即可,不需要强制类型转换。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 04:58:15