NodeJS中JEST测试请求无效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

