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

Jest+Supertest测试Express错误处理接口时出现请求超时问题

问题根因
  • 核心问题出在全局错误处理中间件的错误写法:你在调用res.status(500).send()完成响应发送后,又主动执行throw new Error(error)。这个抛出的错误已经脱离Express的请求处理链路,没有任何后续中间件可以捕获,会直接触发进程级的uncaughtException。Supertest是服务端集成测试工具,会等待整个请求处理链路完全结束才返回结果,这个未捕获异常会直接挂起请求链路,直到测试触发超时。而Postman、Insomnia是独立的HTTP客户端,只要收到服务端返回的500响应报文就会直接渲染结果,不会关心服务端后续抛出的未捕获异常,所以接口调试时看起来运行正常。
  • 不管是访问错误URL触发的404类错误、还是MongoDB抛出的ValidationError,只要最终走到这个全局错误处理中间件,都会触发完全相同的超时问题,和具体的错误类型没有关系。
  • 测试代码本身存在兼容问题:混用async/await和done回调是Jest测试的常见问题,两者同时使用时很容易出现回调未触发、测试挂起的情况;另外你直接导入带app.listen()逻辑的服务入口文件,测试导入时会自动启动服务占用端口,也会干扰Supertest的正常连接。
修复方案

1. 调整服务端代码

首先拆分服务启动逻辑和Express实例导出逻辑,修正错误处理中间件的反模式写法:

const express = require("express");
const app = express();

app.get("/", function(req, res, next) {
  // 显式传递错误
  return next("my error");
});

// 全局错误处理中间件
app.use(function(error, req, res, next) {
  // 如果响应已经发送,直接把错误传递给后续默认处理逻辑,不要重复操作响应
  if (res.headersSent) {
    return next(error);
  }
  // 错误日志打印、错误上报等逻辑要放在发送响应之前执行
  console.error(error);
  // 正常返回错误响应,不要在res.send之后抛出未捕获错误
  res.status(500).send({ 
    error: typeof error === "string" ? error : error.message 
  });
});

// 仅在直接运行该文件时启动服务,被测试文件导入时不执行listen
if (require.main === module) {
  app.listen(8080, function() {
    console.log("server running on 8080");
  });
}

// 导出app实例供测试使用
module.exports = app;

注意:错误处理中间件的核心职责是将错误转换为标准HTTP响应返回给客户端,禁止在调用res.send()之后抛出未捕获错误,这类错误会绕过Express的所有处理链路,直接变成进程级异常,轻则挂起请求,重则导致服务进程崩溃。如果需要做错误上报,在发送响应前执行上报逻辑即可。如果要处理404不存在的路由,在所有路由定义的最后添加404处理中间件,将404错误传递给全局错误处理逻辑即可。

2. 调整测试用例代码

去掉混用的async/await和done回调,正确mock日志方法,修正断言逻辑:

const request = require("supertest");
const app = require("../../app.js");

// 测试前mock console.error,避免测试输出冗余日志
beforeAll(() => {
  jest.spyOn(global.console, "error").mockImplementation(() => {});
});

// 测试完成后恢复所有mock
afterAll(() => {
  jest.restoreAllMocks();
});

it("returns 500 status and error message correctly when route throws", async () => {
  const res = await request(app).get("/");

  // 断言响应状态码
  expect(res.status).toBe(500);
  // 断言返回的错误内容
  expect(res.body.error).toContain("my error");
  // 断言错误日志被正确打印
  expect(console.error).toHaveBeenCalledWith("my error");
});

3. 特殊场景兼容(不推荐生产使用)

如果你由于特殊原因必须保留res.send()之后抛出错误的写法,需要在测试中主动捕获该未捕获异常,避免请求挂起:

it("can catch error thrown after response sent", async () => {
  const uncaughtHandler = jest.fn();
  process.on("uncaughtException", uncaughtHandler);

  const res = await request(app).get("/");
  expect(res.status).toBe(500);
  // 等待事件循环清空,确保抛出的错误被监听捕获
  await new Promise(resolve => setImmediate(resolve));
  // 断言错误确实被抛出
  expect(uncaughtHandler).toHaveBeenCalled();

  // 移除事件监听,避免影响其他测试用例
  process.removeListener("uncaughtException", uncaughtHandler);
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 09:45:38