Jest单元测试进程未优雅退出问题求助(已用临时方案)
问题现象
运行Jest单元测试时收到警告:
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
尝试添加--detectOpenHandles参数时出现错误,目前临时使用package.json中的命令:
"test:ci": "jest --runInBand --forceExit"
但仍会收到提示使用--detectOpenHandles的警告,且这个方案只是强制退出,并非根本解决办法。
已执行的清理操作
已经在测试中添加了数据库连接关闭逻辑:
afterAll(async () => { if (mongo) { await mongo.stop(); } mongoose.connection.close(); });
单元测试代码
import request from 'supertest'; import { app } from '../../app'; it('fails when a email that does not exist is supplied', async () => { await request(app) .post('/api/users/signin') .send({ email: 'test@test.com', password: 'password', }) .expect(400); }); it('fails when an incorrect password is supplied', async () => { await request(app) .post('/api/users/signup') .send({ email: 'test@test.com', password: 'password', }) .expect(201); await request(app) .post('/api/users/signin') .send({ email: 'test@test.com', password: 'aslkdfjalskdfj', }) .expect(400); }); it('responds with a cookie when given valid credentials', async () => { await request(app) .post('/api/users/signup') .send({ email: 'test@test.com', password: 'password', }) .expect(201); const response = await request(app) .post('/api/users/signin') .send({ email: 'test@test.com', password: 'password', }) .expect(200); expect(response.get('Set-Cookie')).toBeDefined(); });
排查与解决建议
1. 修复数据库连接关闭的异步逻辑
mongoose.connection.close()是异步操作,当前代码没有加await,会导致Jest在连接未完全关闭时尝试退出。修改afterAll:
afterAll(async () => { if (mongo) { await mongo.stop(); } await mongoose.connection.close(); // 新增await等待连接关闭完成 });
2. 关闭Express服务器实例
测试中使用supertest请求的Express应用如果未关闭,会持有HTTP服务器句柄导致泄漏。需要在应用文件中导出服务器引用,然后在测试中关闭:
- 比如
app.ts中修改导出:
import express from 'express'; const app = express(); // ... 其他路由和配置 const server = app.listen(process.env.PORT || 3000); export { app, server };
- 在测试文件中更新
afterAll:
import { app, server } from '../../app'; afterAll(async () => { // 先清理数据库 if (mongo) { await mongo.stop(); } await mongoose.connection.close(); // 关闭服务器,等待回调确认完成 await new Promise(resolve => server.close(resolve)); });
3. 清除测试间的数据库状态污染
当前测试中,前一个测试创建的用户会残留到后续测试,不仅可能导致测试不稳定,也可能因数据库连接未正确重置引发泄漏。建议在每个测试前清空数据:
import { User } from '../../models/user'; // 导入你的用户模型 beforeEach(async () => { await User.deleteMany({}); // 清空用户集合 });
4. 排查定时器泄漏
检查业务代码或测试代码中是否有未清理的setTimeout/setInterval,这类定时器会阻止Jest优雅退出。处理方式:
- 对非必要的定时器调用
unref(),允许进程在定时器未完成时退出:
const timer = setTimeout(() => {}, 1000); timer.unref();
- 或者在测试完成后主动清除:
afterEach(() => { // 清除所有定时器 jest.clearAllTimers(); });
5. 定位具体泄漏点
如果--detectOpenHandles全局运行出错,可以尝试单独运行某个测试文件来定位:
jest --detectOpenHandles tests/users/signin.test.ts
逐步缩小范围,找到引发泄漏的具体测试或代码逻辑。
内容的提问来源于stack exchange,提问作者Iynga Iyngaran Iyathurai

