MongoMemoryServer/Nest测试进程无法优雅退出问题求助
使用Mongoose内存测试工具时的测试清理与超时问题
背景与代码
Mongoose内存测试工具代码
import { MongooseModule, MongooseModuleOptions } from '@nestjs/mongoose'; import { MongoMemoryServer } from 'mongodb-memory-server'; import { connection } from 'mongoose'; let mongod: MongoMemoryServer; export const rootMongooseTestModule = (options: MongooseModuleOptions = {}) => MongooseModule.forRootAsync({ useFactory: async () => { mongod = await MongoMemoryServer.create(); const mongoUri = await mongod.getUri(); return { uri: mongoUri, ...options, }; }, }); export const closeInMongodConnection = async () => { connection.dropDatabase(); connection.close(); if (mongod) await mongod.stop(); }; export const removeCollections = async () => { const collections = connection.collections; for (const key in collections) { const collection = collections[key]; await collection.deleteMany({}); } }
UserController测试代码
import { Test, TestingModule } from '@nestjs/testing'; import { MongooseModule } from '@nestjs/mongoose'; import { closeInMongodConnection, removeCollections, rootMongooseTestModule, } from '../../test/utils/MongooseTestModule'; import { UserSchema } from './user.schema'; import { UserController } from './user.controller'; import { UserService } from './user.service'; describe('UserController', () => { let controller: UserController; const mockUser = { username: 'kyle', email: 'kyle@example.com', password: 'password', }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [UserController], imports: [ rootMongooseTestModule(), MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]), ], providers: [UserService], }).compile(); controller = module.get<UserController>(UserController); }); afterAll(async () => { await closeInMongodConnection(); }); afterEach(async () => { await removeCollections(); }); it('should be defined', () => { expect(controller).toBeDefined(); }); describe('create', () => { it('should return the saved object', async () => { const createdUser = await controller.create(mockUser); expect(createdUser.username).toBe(mockUser.username); }); }); });
遇到的问题
- 测试执行完成后,运行
npm test --detectOpenHandles或npm test --runInBand --detectOpenHandles时,仍收到警告:
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.
测试实际已全部通过,无挂起:
Test Suites: 15 passed, 15 total Tests: 16 passed, 16 total Snapshots: 0 total Time: 5.369 s
- 在
closeInMongodConnection函数中添加await connection.dropDatabase();后,测试耗时增加约3秒,且出现超时错误:
thrown: "Exceeded timeout of 5000 ms for a hook.
Use jest.setTimeout(newTimeout) to increase the timeout value, if this is a long-running test."
解决方法
1. 修复清理函数的异步操作
原closeInMongodConnection中的dropDatabase和close未加await,导致异步操作未完成就继续执行,引发资源泄漏。修改后:
export const closeInMongodConnection = async () => { await connection.dropDatabase(); await connection.close(); if (mongod) await mongod.stop(); };
2. 优化测试生命周期,减少Mongo实例启停次数
当前每个测试用例通过beforeEach重新创建模块,会重复启动Mongo内存实例,这是耗时的核心原因。改为在beforeAll中一次性创建模块,afterAll统一清理:
describe('UserController', () => { let controller: UserController; let module: TestingModule; // 保存模块引用 const mockUser = { username: 'kyle', email: 'kyle@example.com', password: 'password', }; beforeAll(async () => { module = await Test.createTestingModule({ controllers: [UserController], imports: [ rootMongooseTestModule(), MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]), ], providers: [UserService], }).compile(); controller = module.get<UserController>(UserController); }); afterAll(async () => { await module.close(); // 先关闭Nest模块释放资源 await closeInMongodConnection(); }); afterEach(async () => { await removeCollections(); // 仅在每个测试后清理集合数据 }); // 测试用例保持不变... });
3. 调整超时时间(可选)
如果清理操作确实耗时,可以在测试文件顶部增加超时设置:
jest.setTimeout(10000); // 调整为10秒,根据实际情况修改
问题根源说明
- 资源泄漏警告:清理函数中的异步操作未加
await,导致Mongo连接、数据库清理未完成就终止进程,残留未释放的句柄。 - 超时错误:每个测试用例重复启停Mongo实例,加上
dropDatabase的开销,导致钩子函数执行时间超过默认的5秒超时限制。
内容的提问来源于stack exchange,提问作者Kyle Corbin Hurst
相关产品推荐
相关产品推荐

