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

NodeJS中JEST测试请求无效ID时出现超时错误的原因咨询

问题:API测试中无效ID请求超时的排查

问题详情

我在测试API接口时,前两个获取有效资源的测试都正常通过,但最后一个请求无效ID的测试始终报Timeout Error,哪怕把超时时间调到30秒也解决不了。奇怪的是,同样的无效ID请求在Postman里能正常返回404错误;如果把测试代码里的const tourId = 'invalid-user-id';改成正确的ID,测试就能正常运行,不会超时。

相关代码

测试代码

import supertest from 'supertest';
import app from '../app';
import dotenv from 'dotenv';
import mongoose from 'mongoose';

dotenv.config({ path: `${__dirname}/../../config.env` });

let DB = '';
if (process.env.DATABASE && process.env.DATABASE_PASSWORD) {
  DB = process.env.DATABASE.replace(
    '<PASSWORD>',
    process.env.DATABASE_PASSWORD
  );
}

/* Connecting to the database before each test. */
mongoose.set('strictQuery', true);

beforeEach(async () => {
  await mongoose.connect(DB);
});

afterEach(async () => {
  await mongoose.connection.close();
});


describe('tours', () => {
  describe('get tours', () => {
    describe('given the tours does exist', () => {
      it('should return a 200', async () => {
        const res = await supertest(app).get('/api/v1/tours');
        expect(res.status).toBe(200);
      });
    });
  });
});

describe('tour', () => {
  describe('get tour', () => {
    describe('given the tour does exist', () => {
      it('should return a 200', async () => {
        const tourId = '5c88fa8cf4afda39709c296c';
        const res = await supertest(app).get(`/api/v1/tours/${tourId}`);
        expect(res.status).toBe(200);
      });
    });
  });
});

describe('tour', () => {
  describe('get tour', () => {
    describe('given the tour does not exist', () => {
      it('should return a 404', async () => {
        const tourId = 'invalid-user-id';
        const res = await supertest(app).get(`/api/v1/tours/${tourId}`);
        console.log(res);
        expect(res.status).toBe(404);
      });
    });
  });
});

接口处理函数(getOne)

export const getOne = (Model: Model<any>, popOptions?: popOptionsType) =>
  catchAsync(async (req, res, next) => {
    let query = Model.findById(req.params.id);
    if (popOptions) query = query.populate(popOptions);
    const doc = await query;
    if (!doc) {
      return next(new AppError('No document found with that ID ', 404));
    }
    res.status(200).json({ status: 'success', results: doc });
  });

AppError自定义错误类

class AppError extends Error {
  status: string;
  statusCode: number;
  isOperational: boolean;
  constructor(message: string, statusCode: number) {
    super(message); //Message is the parameter built in Error accept
    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor); //When a new object is created constructor function is called and that constructor will not appear in the stack trace
  }
}

export default AppError;

可能的原因排查

1. catchAsync函数存在错误

你用catchAsync包裹了异步处理逻辑,但未提供该函数代码。如果catchAsync没有正确捕获异步错误并传递给next(),当Model.findById执行(哪怕返回null)或其他异步操作出错时,错误会被吞掉,导致请求一直处于pending状态,最终超时。

正确的catchAsync实现应该是:

const catchAsync = (fn) => {
  return (req, res, next) => {
    fn(req, res, next).catch(next);
  };
};

如果你的catchAsync没有用.catch(next)把错误传递给Express的错误处理链,就会导致错误无法被处理,请求挂起。

2. 缺少全局错误处理中间件

Express需要一个全局错误处理中间件来接收next(AppError)传递的错误,并返回响应。如果你的应用没有注册这个中间件,当调用next(new AppError(...))时,Express不会自动返回404响应,请求会一直挂着直到超时。

全局错误处理中间件示例:

app.use((err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'error';

  res.status(err.statusCode).json({
    status: err.status,
    message: err.message
  });
});

需确保该中间件注册在所有路由之后。

3. MongoDB ObjectID格式验证缺失

invalid-user-id不是有效的MongoDB ObjectID格式,Model.findById会直接返回null触发next(AppError),但如果此时错误处理链断裂,就会导致无响应。可以在路由层提前验证ID格式,提前返回错误:

import { isValidObjectId } from 'mongoose';

// 在getOne函数里先验证ID
export const getOne = (Model: Model<any>, popOptions?: popOptionsType) =>
  catchAsync(async (req, res, next) => {
    if (!isValidObjectId(req.params.id)) {
      return next(new AppError('Invalid ID format', 400));
    }
    let query = Model.findById(req.params.id);
    // ... 剩余逻辑
  });

这样能提前拦截无效格式的ID,避免后续错误处理链问题。

4. 测试环境数据库连接/关闭逻辑异常

虽然前两个测试正常,但第三个测试因请求挂起,可能导致afterEach里的mongoose.connection.close()无法执行,或数据库连接状态异常。不过该可能性较低,因为Postman请求正常,说明核心业务逻辑无问题,仍需优先排查错误处理链。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 17:35:01