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
相关产品推荐
相关产品推荐

