测试清理时触发MongoTopologyClosedError的原因及解决方法
Jest中关闭MongoDB连接抛出MongoTopologyClosedError的问题排查与解决
问题场景
我在针对本地Docker部署的MongoDB执行集成测试时,在Jest的清理钩子中编写了关闭MongoDB连接的代码:
import { client } from "../lib/common/mongodb"; module.exports = async function () { await client.close(); };
但每次执行都会抛出以下错误,导致测试无法正常退出:
/home/ian/src/my-proj/node_modules/mongodb/lib/sdam/topology.js:217 drainWaitQueue(this[kWaitQueue], new error_1.MongoTopologyClosedError()); ^ MongoTopologyClosedError: Topology is closed at /home/ian/src/my-proj/node_modules/mongodb/src/sdam/topology.ts:493:46 { [Symbol(errorLabels)]: Set(0) {} } Node.js v20.2.0 error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
MongoClient单例实现
项目中使用单例模式管理MongoClient实例,代码如下:
let client; let clientPromise; if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test") { // 开发模式下使用全局变量,保留HMR导致模块重载时的值 let globalWithMongo = global; const uri = MONGO_SERVER_URL.replace("<username>", MONGO_USERNAME).replace("<password>", MONGO_PASSWORD); if (!globalWithMongo._mongoClientPromise) { client = new MongoClient(uri, options); globalWithMongo._mongoClientPromise = client.connect(); } clientPromise = globalWithMongo._mongoClientPromise; } else { // 生产模式下建议不使用全局变量 client = new MongoClient(uri, options); clientPromise = client.connect(); } // 导出模块作用域的MongoClient Promise,实现客户端跨函数共享 export default clientPromise; export { client };
临时规避方案
目前通过捕获未处理的拒绝来强制退出进程,但这只是临时hack:
import { client } from "../lib/common/mongodb"; module.exports = async function () { process.on('unhandledRejection', (e) => { console.log("This is a hack, to deal with MongoDB not shutting down properly. It does not interfere with Jest test failures"); process.exit(0); }); await client.close(); };
原因分析
- 重复执行关闭操作:Jest的测试环境可能会因为模块缓存、测试套件的钩子配置等原因,导致清理逻辑被多次调用。单例MongoClient实例在第一次关闭后,再次调用
close()就会触发拓扑已关闭的错误。 - 全局变量的测试隔离问题:在test环境下使用全局变量保存连接Promise,但Jest的测试隔离机制(如
resetModules)可能导致模块被重新加载,此时导出的client可能指向已经被关闭的实例。 - 未检查连接状态:直接调用
client.close()前没有判断连接是否处于打开状态,当连接已经关闭时就会抛出错误。
调试方法
- 检查连接状态:在清理函数中添加日志,确认调用
close()时的连接状态:console.log('当前连接状态是否已关闭:', client.topology.isClosed()); await client.close(); - 追踪清理函数执行次数:添加日志查看清理逻辑是否被多次触发:
console.log('清理函数执行'); await client.close(); - 监控单例实例创建:在MongoClient单例代码中添加日志,确认实例是否被重复创建或复用:
if (!globalWithMongo._mongoClientPromise) { console.log('创建新的MongoClient实例'); client = new MongoClient(uri, options); globalWithMongo._mongoClientPromise = client.connect(); } else { console.log('复用已有的MongoClient连接Promise'); }
正确解决办法
1. 先检查连接状态再关闭
在清理函数中判断连接是否处于打开状态,仅当连接未关闭时执行关闭操作:
import { client } from "../lib/common/mongodb"; module.exports = async function () { if (!client.topology.isClosed()) { await client.close(); } };
2. 捕获并忽略特定错误
如果无法避免重复调用,可捕获MongoTopologyClosedError并忽略,其他错误正常抛出:
import { client } from "../lib/common/mongodb"; import { MongoTopologyClosedError } from 'mongodb'; module.exports = async function () { try { await client.close(); } catch (err) { if (!(err instanceof MongoTopologyClosedError)) { throw err; } } };
3. 优化单例的测试环境管理
在测试结束后清理全局变量,避免后续测试复用已关闭的连接实例:
import { client } from "../lib/common/mongodb"; module.exports = async function () { if (!client.topology.isClosed()) { await client.close(); } // 清理全局变量,确保下一次测试能创建新的连接 delete global._mongoClientPromise; };
4. 确保清理逻辑只注册一次
如果使用setupFilesAfterEnv注册清理钩子,确保只注册一次,避免重复执行:
// setupTests.js let cleanupRegistered = false; if (!cleanupRegistered) { afterAll(async () => { const { client } = require("../lib/common/mongodb"); if (!client.topology.isClosed()) { await client.close(); } }); cleanupRegistered = true; }
内容的提问来源于stack exchange,提问作者Ian
相关产品推荐
相关产品推荐

