使用Vitest测试集成@fastify/websocket的Fastify后端时,测试执行完成却仍超时
看起来你遇到的是典型的测试资源未及时清理导致Vitest误判测试上下文未结束的问题——虽然你的测试代码逻辑上已经跑完全部断言,但事件循环里还残留着未彻底释放的活跃句柄(比如半关闭的WebSocket连接、Fastify实例资源),导致Vitest一直在等事件循环清空,最后触发超时。结合你的代码和依赖版本,我给你几个排查和解决的方向:
1. 调整afterEach的资源清理顺序
你现在的afterEach是先关闭Fastify实例,再终止WebSocket连接,这个顺序可能搞反了:Fastify关闭时会尝试优雅关闭所有WebSocket连接,但此时你的测试用ws客户端还处于半活跃状态,反而会拖慢清理速度,甚至残留句柄。
把顺序反过来,先强制终止WebSocket连接,再关闭Fastify实例:
afterEach(async () => { // 先切断客户端WebSocket连接,避免Fastify关闭时等待连接优雅断开 ws?.terminate(); // 再关闭Fastify实例,释放所有服务器端资源 await app.close(); });
2. 强化Fastify实例的WebSocket清理逻辑
Fastify 5.x和@fastify/websocket 11.x的组合下,服务器端处理错误关闭后,可能还有未清理的socket句柄。你可以在构建Fastify实例时,显式配置websocket插件的清理选项,或者添加钩子确保所有连接被销毁:
// 假设你的buildApp函数是创建Fastify实例的地方 async function buildApp() { const app = fastify(); app.register(require('@fastify/websocket'), { // 确保服务器端在关闭时强制清理所有websocket连接 closeOnTrailers: true }); // 额外添加钩子,手动清理所有活跃的websocket连接 app.addHook('onClose', async (instance) => { const websocketServer = instance.websocketServer; if (websocketServer) { websocketServer.clients.forEach(client => { if (client.readyState !== WebSocket.CLOSED) { client.terminate(); } }); } }); // 其他路由和配置... return app; }
3. 给测试Promise加兜底,避免无限等待
虽然你用了ws.once('close')(触发后会自动移除监听器),但可以给测试的Promise加一个超时兜底,同时手动做监听器清理的保险:
it('disconnects if the client sends malformed JSON', async () => { const { promise, resolve } = Promise.withResolvers<CloseMessage>(); const closeHandler = (code, reason) => { resolve({ code, reason }); }; ws.once('close', closeHandler); // 给测试Promise加兜底超时,避免因close事件未触发导致的无限等待 const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Close event timeout')), 5000) ); const racePromise = Promise.race([promise, timeoutPromise]); ws.send('fgsfds'); // 发送无效JSON const { code } = await racePromise; // 你的断言逻辑... console.log("tests are done"); // 手动移除监听器(虽然once会自动移除,但保险起见) ws.off('close', closeHandler); }, 10000); // 给整个测试设置更长的超时时间,避免被Vitest默认超时打断
4. 触发事件循环收尾,确保无残留任务
Node 22.x的事件循环检测比旧版本更严格,Vitest 3.x会等待事件循环完全清空才会标记测试结束。你可以在测试最后加一行,让Node.js处理完所有待处理任务:
// 在所有断言之后添加 await new Promise(resolve => setImmediate(resolve));
这会让Node.js处理完所有微任务和宏任务,确保没有残留的活跃句柄。
另外,你提到console.log的内容被归因到下一个测试,这也侧面证明了测试之间的隔离性出了问题——前一个测试的WebSocket连接或Fastify实例没清理干净,导致下一个测试的上下文被污染。调整afterEach的清理顺序应该能同步解决这个问题。
内容来源于stack exchange

