Express Supertest测试控制器异常路径时持续出现超时错误求助
我之前也碰到过几乎一模一样的测试超时问题,咱们一步步拆解可能的原因:
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

