Jasmine隔离测试两类EventEmitter导入冲突致测试失败问题
Angular MugComponent隔离单元测试问题排查与修复
核心问题根因
- 两类EventEmitter命名冲突:Node.js
events模块导出的EventEmitter和@angular/core提供的EventEmitter同名,直接导入会发生覆盖,无法同时使用 - 订阅报错
Cannot read properties of undefined (reading 'subscribe'):basicService.siteChange是带subscribe方法的属性(Angular EventEmitter/Subject类型),不是类方法。使用jasmine.createSpyObj创建mock时,仅把siteChange放在方法列表里不会初始化该属性,实际值为undefined - 弹窗报错
Cannot read properties of undefined (reading 'componentInstance'):modalService.open是jasmine创建的spy,默认返回值为undefined,没有mock返回值中的componentInstance层级结构,调用时自然读不到属性 - 测试逻辑缺陷:
ngOnInit中仅对siteChange注册了订阅,没有触发流发射值的话,订阅回调不会执行,仅调用ngOnInit+tick()不会触发getRelatedMugs调用openModal中仅给Node EventEmitter注册了contentEvent监听,没有主动触发事件的话,setContent不会被调用mugService.filterC是对象类型属性,错放在方法列表里创建为spy函数,赋值id属性时会报错- 没有给组件自身的
setContent方法创建spy,无法断言调用情况
排查与修复步骤
- 用ES模块别名语法解决同名导入冲突,给两个来源的EventEmitter设置不同别名
- 区分服务的方法和属性:
jasmine.createSpyObj第二个参数仅传方法名列表,属性、值类型成员统一放在第三个参数的对象中初始化 - 补全链式调用的mock返回值:逐层mock
modalService.open()返回的弹窗引用对象,包含componentInstance和其下的showComponent方法 - 给需要断言的组件自身方法(如
setContent)创建spy - 事件类逻辑手动触发:测试环境下不会自动触发事件回调,需要手动调用emit方法发射对应事件,验证回调逻辑
修复后的完整测试代码
// 同名类使用别名导入,避免冲突 import { EventEmitter as NodeEventEmitter } from "events"; import { EventEmitter as NgEventEmitter } from "@angular/core"; import { fakeAsync, tick } from "@angular/core/testing"; // 其余业务依赖按项目路径正常导入:MugComponent、Constants、MugService、BasicService、ToolsService、NgbModal、ModalContainerComponent等 describe("MugComponent", () => { let component: MugComponent; const constants: Constants = new Constants(); // 提前mock弹窗实例,补全componentInstance层级 const mockModalRef = { componentInstance: { showComponent: jasmine.createSpy("showComponent") } }; // 创建各服务mock,区分方法和属性 let mugService = jasmine.createSpyObj<MugService>("mugService", ["getRelatedMugs"], // 方法放第二个参数 { filterC: { id: null } } // 属性放第三个参数初始化 ); let modalService = jasmine.createSpyObj<NgbModal>("modalService", ["open"]); let basicService = jasmine.createSpyObj<BasicService>("basicService", [], { siteChange: new NgEventEmitter<number>() } // Angular类型的EventEmitter初始化siteChange ); let toolsService = jasmine.createSpyObj<ToolsService>("toolsService", ["toRoute"], { eventEmitter: new NodeEventEmitter() } // Node类型的EventEmitter初始化toolsService的事件实例 ); beforeEach(() => { // 重置所有spy调用记录 jasmine.resetAllSpies(); // 给modal.open设置固定返回值 modalService.open.and.returnValue(mockModalRef); // 实例化组件 component = new MugComponent( constants, mugService, basicService, toolsService, modalService, ); component.mug = new Mug(); // 给组件自身方法加spy,用于断言 spyOn(component, "setContent").and.stub(); }); it("should update filterC and call getRelatedMugs when siteChange emits value", fakeAsync(() => { component.ngOnInit(); // 手动触发siteChange发射值,激活订阅回调 const testSiteId = 2; basicService.siteChange.emit(testSiteId); tick(); expect(mugService.filterC.id).toBe(testSiteId); expect(mugService.getRelatedMugs).toHaveBeenCalled(); })); it("should open modal and call setContent when contentEvent is triggered", fakeAsync(() => { const testContentType = "material"; const testContent = { name: "ceramic" }; component.openModal(testContentType); // 验证弹窗打开逻辑 expect(modalService.open).toHaveBeenCalledWith(ModalContainerComponent, { size: "lg", backdrop: "static", }); expect(mockModalRef.componentInstance.showComponent).toHaveBeenCalled(); // 手动触发Node EventEmitter的contentEvent事件,激活回调 toolsService.eventEmitter.emit("contentEvent", testContent); tick(); expect(component.setContent).toHaveBeenCalledWith(testContentType, testContent); })); });
额外注意点
- 隔离单元测试直接new组件实例、手动mock所有依赖的方式,比使用TestBed渲染组件执行速度快3~10倍,更适合纯逻辑类的测试
- 所有基于订阅、事件监听的逻辑,测试时必须手动触发流/事件发射值,仅靠
tick()、flush()不会自动执行未触发的回调 - 若项目中多处混用两类EventEmitter,建议封装统一的类型别名,避免每个测试文件都重复写别名导入
内容的提问来源于stack exchange,提问作者Ivan Martin Blanco
相关产品推荐
相关产品推荐

