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

Jest mockRejectedValue捕获异常为undefined的原因与修复方法

问题背景

编写Jest + React Testing Library单元测试时,需要验证:点击按钮触发异步endCall逻辑时,如果flex.Actions.invokeAction抛出指定message的异常,代码会正确调用datadogLogs.logger.warn方法。
测试代码如下:

it("throws expected error when invoke action promise is rejected", async () => {
    const datadogWarnSpy = jest.spyOn(datadogLogs.logger, "warn");
    const invokeActionSpy = jest
      .spyOn(flex.Actions, "invokeAction")
      .mockRejectedValue("no active call");

    const SUT = withConference((props: WithConferenceProps) => (
      <button onClick={() => props.outboundConference.controls.endCall()}>
        End Call
      </button>
    ));

    task.conference.liveWorkerCount = 1;
    render(
      <SUT flex={flex} task={task} conferenceService={conferenceService} />,
      { store }
    );

    userEvent.click(screen.getByText(/end call/i));

    await waitFor(() => {
      expect(invokeActionSpy).toThrow("no active call");
    });

    await waitFor(() => {
      expect(datadogWarnSpy).toHaveBeenCalled();
    });
});

对应的被测业务代码如下:

private endCall = async (other?: {
      taskSid: string;
      reservationSid: string;
}): Promise<void> => {
  try {
    const { task, flex } = this.props;
    
    if (
      this.getParticipants(other?.reservationSid).length ||
      task.conference?.liveWorkerCount
    ) {
      const payload = other?.reservationSid
        ? { sid: other.reservationSid }
        : { task };
        await flex.Actions.invokeAction("HangupCall", payload);
      }
    } catch (e: any) {
      // 注意:此处贴的代码有拼写错误,messsage应为message
      if (e.messsage === NO_ACTIVE_CALL_MSG) {
        datadogLogs.logger.warn(`endCall: ${e.message}`);
      } else {
        datadogLogs.logger.error(`endCall: ${e.message}`);
      }
    } finally {
        this.endRecording(other?.taskSid);
    }
};

测试运行时出现异常:catch块中捕获到的e始终为undefined,导致代码走到error日志分支,测试失败,但同一段业务代码在线上运行完全正常。

根本原因

两个问题叠加导致异常捕获不符合预期:

  • 断言写法完全错误:expect(invokeActionSpy).toThrow("no active call") 是Jest提供的同步错误断言语法,执行时Jest会主动调用传入的invokeActionSpy函数,校验其同步执行过程是否抛出错误。但invokeAction是异步函数,被mockRejectedValue处理后只会返回rejected状态的Promise,同步执行阶段不会抛出任何错误,这个断言会直接触发失败。更严重的是,Jest主动调用spy生成的rejected Promise没有被catch处理,会触发JS环境的未捕获Promise拒绝异常,打乱测试的异步执行时序,被测代码的catch块还没正常接收到异常,测试流程就被中断,最终拿到的e为undefined。
  • Mock值不符合线上真实逻辑:你用.mockRejectedValue("no active call") 传入的是原始字符串,而线上环境invokeAction抛出的是标准Error实例,自带message属性。就算时序正常,catch块拿到的e是字符串类型,访问e.message会得到undefined,也无法进入warn分支。另外你贴的业务代码里e.messsage拼写错误(三个s),如果代码里真这么写也会导致判断失效,需要修正为e.message。
修复方案

按以下几点修改测试代码即可:

  • 删除错误的toThrow同步断言,不需要额外校验spy的抛错逻辑,只需要等待最终的副作用(warn方法被调用)符合预期即可。如果确实要校验异步reject逻辑,需要用await expect(promise).rejects.toThrow()的异步断言写法,不要直接对spy用toThrow。
  • 修正mock的拒绝值,传入和线上行为一致的Error实例,而非原始字符串。
  • 如果使用v14及以上版本的@testing-library/user-event,userEvent.click是异步方法,需要加await等待点击的同步逻辑执行完成。
  • 测试结束后记得restore spy,避免影响其他用例。

修改后的完整测试代码:

it("throws expected error when invoke action promise is rejected", async () => {
    const datadogWarnSpy = jest.spyOn(datadogLogs.logger, "warn");
    const invokeActionSpy = jest
      .spyOn(flex.Actions, "invokeAction")
      // 修正:传入Error实例,和线上真实抛出的异常结构一致
      .mockRejectedValue(new Error("no active call"));

    const SUT = withConference((props: WithConferenceProps) => (
      <button onClick={() => props.outboundConference.controls.endCall()}>
        End Call
      </button>
    ));

    task.conference.liveWorkerCount = 1;
    render(
      <SUT flex={flex} task={task} conferenceService={conferenceService} />,
      { store }
    );

    // 新版userEvent需要await
    await userEvent.click(screen.getByText(/end call/i));

    // 只需要等待最终预期的副作用执行即可
    await waitFor(() => {
      expect(datadogWarnSpy).toHaveBeenCalledWith(`endCall: no active call`);
    });

    // 可选:校验invokeAction的调用参数符合预期
    expect(invokeActionSpy).toHaveBeenCalledWith("HangupCall", { task });

    // 清理spy
    datadogWarnSpy.mockRestore();
    invokeActionSpy.mockRestore();
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 06:06:40