You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 03:25:17