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

