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

Express Supertest测试控制器异常路径时持续出现超时错误求助

排查Express登录控制器异常路径测试超时的思路

我之前也碰到过几乎一模一样的测试超时问题,咱们一步步拆解可能的原因:

1. 先确认自定义APIError的正确性

你抛出的APIError必须是能被错误中间件正确识别的类型,重点检查是否正确挂载了statusCode属性:

class APIError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode; // 这行绝对不能少!
    this.name = 'APIError';
    Error.captureStackTrace(this, this.constructor); // 可选,但能帮你更方便调试
  }
}

如果statusCode没被正确设置,错误中间件可能无法识别这是401错误,导致没有发送响应,请求就会一直挂着超时。

2. 验证User.findOne的异步逻辑是否正确触发

测试用户不存在的场景时,要确保User.findOne({username: ...}).exec()确实返回null:

  • 如果用真实数据库测试:务必清空测试库,或者确保notExistInDB这个用户名真的不存在
  • 如果用Sinon桩化:检查stub是否正确返回resolved的null:
// 正确的桩化方式
sinon.stub(User, 'findOne').returns({
  exec: sinon.stub().resolves(null)
});

// 测试后记得恢复,避免影响其他测试用例
after(() => {
  User.findOne.restore();
});

如果桩化的exec()没有正确resolves,而是处于pending状态,整个Promise链会直接卡住,导致超时。

3. 检查bcrypt.compareAsync的合法性

注意bcrypt原生是回调式API,如果你用了compareAsync,必须确保它是一个返回Promise的方法:

  • 如果你是用util.promisify转换的,要确认转换正确:
const bcrypt = require('bcrypt');
const { promisify } = require('util');
const compareAsync = promisify(bcrypt.compare);

如果compareAsync不是合法的Promise对象,当用户不存在时抛出的错误虽然进入了catch(next),但之前的Promise链可能已经断裂,导致错误中间件没有被触发。

4. 错误处理中间件的完整性

确认你的错误中间件是标准的Express错误处理格式(必须包含4个参数):

app.use((err, req, res, next) => {
  // 优先处理自定义APIError
  if (err instanceof APIError) {
    return res.status(err.statusCode).json({
      message: err.message,
      error: process.env.NODE_ENV === 'development' ? err : {}
    });
  }
  // 其他错误默认处理
  res.status(500).json({ message: 'Internal Server Error' });
});

如果中间件少了next参数,Express不会把它识别为错误处理中间件,错误会被直接忽略,请求自然会超时。

5. 临时添加调试日志定位问题

给测试加个回调,看看请求实际的响应情况,能帮你快速定位问题:

it('throws unauthorized error if no user match', (done) => {
  request(app)
    .post('/login')
    .send({ username: 'notExistInDB', password: 'asdfasd' })
    .end((err, res) => {
      console.log('Error:', err);
      console.log('Response status:', res?.statusCode);
      console.log('Response body:', res?.body);
      done();
    });
});

这样能直接看到是响应没回来,还是返回了其他状态码(比如500),而不是你期望的401,导致测试一直等待超时。

6. 检查Promise链的完整性

看你控制器里的some more chained promises部分,确保在用户不存在抛出错误后,Promise链能正确走到catch(next):

function login(req, res, next) {
  User.findOne({username: req.body.username}).exec()
  .then(user => {
    if (user) {
      return bcrypt.compareAsync(req.body.password, user.password)
    } else {
      throw new APIError("Unauthorized", 401)
    }
  })
  .then(/* 这里的逻辑只在用户存在且密码校验通过时执行 */)
  .catch(next) // 这里必须能捕获所有上游错误,包括你抛出的APIError
}

如果catch(next)没有正确捕获错误(比如中间有其他catch吃掉了错误),也会导致请求挂起。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 09:59:17