API测试全部通过但运行test.js时仍出现超时退出错误
问题
所有API单元测试用例均已通过,但运行test.js时仍收到错误提示:
✘ Timed out while running tests Failed to exit when running test.js
尝试过设置20秒全局超时、添加sinon.restore()恢复stub、给每个测试用例设置10秒超时,均无效果。
相关代码
test.js
const test = require('ava'); const request = require('supertest'); const sinon = require('sinon'); const proxyquire = require('proxyquire'); // Mock database operations const dbMock = { getBikes: sinon.stub().callsFake((callback) => callback(null, [{ id: 1, name: 'Bike 1' }])), getUsers: sinon.stub().callsFake((callback) => callback(null, [{ id: 1, name: 'User 1' }])), getBikeUsage: sinon.stub().callsFake((callback) => callback(null, [{ id: 1, bikeId: 1, userId: 1 }])), saveBikeUsage: sinon.stub().callsFake((bikeUsage, callback) => callback(null, { insertId: 1 })), updateBikeUsageStatus: sinon.stub().callsFake((bikeId, status, callback) => callback(null, {})), unassignBike: sinon.stub().callsFake((bikeId, endTime, callback) => callback(null, {})), updateBikeDetails: sinon.stub().callsFake((id, details, callback) => callback(null, {})), updateUserDetails: sinon.stub().callsFake((id, name, callback) => callback(null, {})), addUser: sinon.stub().callsFake((name, callback) => callback(null, { insertId: 1 })), addBike: sinon.stub().callsFake((bike, callback) => callback(null, { insertId: 1 })), getUserById: sinon.stub().callsFake((userId, callback) => callback(null, [{ id: userId, name: 'User 1' }])), getUserHistory: sinon.stub().callsFake((userId, callback) => callback(null, [{ id: 1, bikeId: 1, userId: userId }])), addCommentToBike: sinon.stub().callsFake((bikeId, comment, callback) => callback(null, {})), }; // Proxyquire to replace './db' with our mock const app = proxyquire('./server', { './db': dbMock }); test.afterEach.always(() => { sinon.restore(); // Restore all stubs and spies after each test }); // Test for GET /bikes endpoint test('GET /bikes returns list of bikes', async (t) => { const res = await request(app).get('/bikes'); t.is(res.status, 200); t.true(Array.isArray(res.body)); t.true(res.body.length > 0); }); // 其余测试用例省略(与原代码一致)
db.js
const mysql = require('mysql2'); // Create a connection pool const pool = mysql.createPool({ host: 'xxxx', user: 'xxxx', password: 'xxxx', database: 'xxxx', waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); module.exports = { getBikes: (callback) => { pool.query('SELECT * FROM Bikes', callback); }, // 其余数据库方法省略(与原代码一致) };
测试结果
flinkregi_backend@1.0.0 test ava Server running on port 3000 ✔ GET /bikes returns list of bikes (146ms) ✔ GET /users returns list of users (145ms) ✔ GET /bike-usage returns list of bike usage history (144ms) ✔ POST /bike-usage saves bike usage data (143ms) ✔ POST /bike-unassign unassigns bike and updates end time (142ms) ✔ PUT /bikes/:id updates bike details (139ms) ✔ PUT /users/:id updates user details (137ms) ✔ POST /users adds new user (138ms) ✔ POST /bikes adds new bike (137ms) ✔ GET /users/:id returns user details for valid ID (136ms) ✔ GET /user-history/:userId returns user history for valid user ID (136ms) ✔ POST /bikes/:bikeId/comments adds comment to bike (137ms) ✘ Timed out while running tests Failed to exit when running test.js ─ 12 tests passed
解决方案
1. 修复服务器启动逻辑
你的server.js大概率直接调用了app.listen(3000),导致测试时服务器持续运行,进程无法退出。修改server.js,让服务器只在直接运行时启动:
// server.js const express = require('express'); const app = express(); // 中间件、路由配置... // 仅在直接执行server.js时启动服务器,测试时由supertest处理 if (require.main === module) { app.listen(3000, () => { console.log('Server running on port 3000'); }); } module.exports = app;
2. 强制关闭数据库连接池(可选)
即使使用了mock,若测试中意外加载了真实的db.js,连接池会保持活跃导致进程不退出。可在测试结束后添加清理逻辑:
// 在test.js中添加全局后置钩子 test.after.always(() => { try { const realDb = require('./db'); if (realDb.pool) { realDb.pool.end(); // 关闭连接池 } } catch (e) { // 忽略错误,因为mock已替换真实db } });
3. 重置Sinon mock状态
将mock创建移到beforeEach中,确保每次测试使用全新的mock,避免残留状态:
let app; test.beforeEach(() => { // 每次测试重新创建mock const dbMock = { getBikes: sinon.stub().callsFake((callback) => callback(null, [{ id: 1, name: 'Bike 1' }])), // 其余mock方法与原代码一致 }; app = proxyquire('./server', { './db': dbMock }); }); test.afterEach.always(() => { sinon.restore(); });
4. 调整AVA全局超时(兜底方案)
若以上方法无效,可在package.json中配置AVA全局超时:
{ "ava": { "timeout": "30s", "failFast": false } }
原因分析
进程无法退出的核心是存在未关闭的活跃资源:
- HTTP服务器:
app.listen()会持有端口监听,保持事件循环活跃 - 数据库连接池:mysql2连接池默认不会自动关闭,持有连接阻止进程退出
- 残留mock状态:若mock未完全重置,可能残留异步任务或资源引用
内容的提问来源于stack exchange,提问作者Giel v Bussel
相关产品推荐
相关产品推荐

