Angular测试报错:Expected a spy, but got Function,但测试仍通过
问题分析:forEach循环内测试Angular组件事件触发报错但测试通过
问题现象
测试Angular组件的多个按钮点击事件时,使用forEach遍历MatButtonHarness实例,会触发错误提示"Expected a spy, but got Function",但测试用例却显示执行成功;仅测试单个按钮时无此报错。
报错测试代码
it("should emit event", async () => { component.type = "confirm"; fixture.detectChanges(); spyOn(component.dialogClose, "emit"); const buttons = await loader.getAllHarnesses(MatButtonHarness); buttons.forEach(async (button) => { await button.click(); expect(component.dialogClose.emit).toHaveBeenCalled(); }); });
无报错测试代码
it("should emit event", async () => { component.type = "confirm"; fixture.detectChanges(); spyOn(component.dialogClose, "emit"); const button = await loader.getHarness(MatButtonHarness); await button.click(); expect(component.dialogClose.emit).toHaveBeenCalled(); });
被测试组件代码
@Output() public dialogClose: EventEmitter<boolean> = new EventEmitter<boolean>();
模板代码
<div mat-dialog-actions [ngSwitch]="type"> ... <ng-container *ngSwitchCase="'confirm'"> <button mat-button color="warn" (click)="dialogClose.emit(false)">Cancel</button> <button mat-raised-button color="accent" (click)="dialogClose.emit(true)">Yes</button> </ng-container> ... </div>
原因解析
- forEach不支持异步等待:
forEach无法识别回调函数中的async/await,遍历过程中会直接启动所有异步回调,不会等待它们完成。测试框架(如Jasmine)会认为测试用例的主逻辑已经执行完毕,提前标记测试为通过。 - spy上下文失效:测试结束后,Angular会自动清理组件实例及相关依赖,此时
component.dialogClose.emit已经不再是之前创建的spy对象。而forEach里的异步回调还在执行,后续的expect断言会因为spy已被销毁而抛出错误,但此时测试已经被标记为通过,所以出现“报错但测试成功”的矛盾现象。 - 单个按钮测试无问题的原因:单个按钮测试时,
await button.click()是在测试用例的主async函数中执行,测试框架会等待该异步操作完成后再执行expect,此时spy仍处于有效状态,不会触发错误。
解决方案
方法一:使用for...of循环替代forEach
for...of可以和await配合,确保每个按钮的点击和断言都按顺序执行,测试框架会等待所有异步操作完成后再结束测试:
it("should emit event", async () => { component.type = "confirm"; fixture.detectChanges(); // 把spy赋值给变量,避免重复引用可能的上下文变化 const emitSpy = spyOn(component.dialogClose, "emit"); const buttons = await loader.getAllHarnesses(MatButtonHarness); for (const button of buttons) { await button.click(); expect(emitSpy).toHaveBeenCalled(); // 可选:每次断言后重置spy调用记录,避免前一次点击影响下一次断言 emitSpy.calls.reset(); } });
方法二:用Promise.all等待所有异步操作完成
将每个按钮的测试逻辑包装成Promise,通过Promise.all等待所有Promise完成,确保所有断言都在测试结束前执行:
it("should emit event", async () => { component.type = "confirm"; fixture.detectChanges(); const emitSpy = spyOn(component.dialogClose, "emit"); const buttons = await loader.getAllHarnesses(MatButtonHarness); await Promise.all(buttons.map(async (button) => { await button.click(); expect(emitSpy).toHaveBeenCalled(); emitSpy.calls.reset(); })); });
内容的提问来源于stack exchange,提问作者Robouste
相关产品推荐
相关产品推荐

