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

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>

原因解析

  1. forEach不支持异步等待:forEach无法识别回调函数中的async/await,遍历过程中会直接启动所有异步回调,不会等待它们完成。测试框架(如Jasmine)会认为测试用例的主逻辑已经执行完毕,提前标记测试为通过。
  2. spy上下文失效:测试结束后,Angular会自动清理组件实例及相关依赖,此时component.dialogClose.emit已经不再是之前创建的spy对象。而forEach里的异步回调还在执行,后续的expect断言会因为spy已被销毁而抛出错误,但此时测试已经被标记为通过,所以出现“报错但测试成功”的矛盾现象。
  3. 单个按钮测试无问题的原因:单个按钮测试时,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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 14:15:32