使用Jest、Supertest、MongoDB与Express测试Express路由时遭遇环境销毁错误的求助
我在使用Jest和Supertest测试Express路由时碰到了棘手的问题:启动应用时我会连接MongoDB并把MongoClient存入app.locals,但一加上这个连接逻辑就会报错;注释掉连接调用后,错误消失但会收到Jest的未退出警告。
报错信息
ReferenceError: You are trying to `import` a file after the Jest environment has been torn down. at BufferList.Readable (server/node_modules/readable-stream/lib/_stream_readable.js:179:22) at BufferList.Duplex (server/node_modules/readable-stream/lib/_stream_duplex.js:67:12) at new BufferList (server/node_modules/bl/bl.js:33:16) at new MessageStream (server/node_modules/mongodb/lib/cmap/message_stream.js:35:21) at new Connection (server/node_modules/mongodb/lib/cmap/connection.js:54:28) /Users/zackchan/Documents/dev/server/node_modules/readable-stream/lib/_stream_readable.js:111 var isDuplex = stream instanceof Duplex; ^ TypeError: Right-hand side of 'instanceof' is not callable at new ReadableState (/Users/zackchan/Documents/dev/server/node_modules/readable-stream/lib/_stream_readable.js:111:25) at BufferList.Readable (/Users/zackchan/Documents/dev/server/node_modules/readable-stream/lib/_stream_readable.js:183:25) at BufferList.Duplex (/Users/zackchan/Documents/dev/server/node_modules/readable-stream/lib/_stream_duplex.js:67:12) at new BufferList (/Users/zackchan/Documents/dev/server/node_modules/bl/bl.js:33:16) at new MessageStream (/Users/zackchan/Documents/dev/server/node_modules/mongodb/lib/cmap/message_stream.js:35:21) at new Connection (/Users/zackchan/Documents/dev/server/node_modules/mongodb/lib/cmap/connection.js:54:28) at /Users/zackchan/Documents/dev/server/node_modules/mongodb/lib/core/connection/connect.js:36:29 at callback (/Users/zackchan/Documents/dev/server/node_modules/mongodb/lib/core/connection/connect.js:280:5) at TLSSocket.connectHandler (/Users/zackchan/Documents/dev/server/node_modules/mongodb/lib/core/connection/connect.js:325:5) at Object.onceWrapper (events.js:421:28)
注释MongoDB连接后的警告
Jest did not exit one second after the test run has completed. This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with
--detectOpenHandlesto troubleshoot this issue.
相关代码
app.js
const https = require('https'); const fs = require('fs'); const path = require('path'); const dotenv = require('dotenv'); const express = require('express'); const rateLimiter = require('express-rate-limit'); const { MongoClient } = require('mongodb'); const app = express(); const port = process.env.PORT || 443; const limit = rateLimiter({ window: 15 * 60 * 1000, max: 100 }); var httpsOptions; if(process.env.NODE_ENV === 'development'){ const rootCA = require('ssl-root-cas').create().addFile(path.join(__dirname, './cert/CA.pem')); https.globalAgent.options.ca = rootCA; httpsOptions = { key: fs.readFileSync(path.join(__dirname, './cert/localhost.key')), cert: fs.readFileSync(path.join(__dirname, './cert/localhost.crt')) }; } MongoClient.connect(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true }, (err, mongoClient) => { if(err) throw err; app.locals.mongoClient = mongoClient; }); app.use(limit); app.use(express.json()); app.get('/', (req, res) => { res.json({path: '/'}); }); const server = https.createServer(httpsOptions, app).listen(port); module.exports = app;
test.js
const dotenv = require('dotenv'); const path = require('path'); process.env = dotenv.config({path: path.resolve(__dirname, '.env')}).parsed; const request = require('supertest'); const app = require('../app'); describe('GET /', () => { it('responds with path of /', (done) => { // app.locals is empty here request(app).get('/').expect(JSON.stringify({path: '/'}), done); }); });
我试过在测试结束后调用app.locals.mongoClient.close(),但这里mongoClient是undefined;也试过把MongoClient.connect()包在async函数里执行,都没效果。求可行的解决思路!
我之前碰到过几乎一模一样的问题,核心就是异步连接和测试生命周期没有对齐,下面几个方案亲测有效:
1. 重构App的MongoDB连接逻辑,改为异步导出
当前你的MongoClient.connect是异步执行的,但你直接同步导出了app,导致测试启动时连接还没建立(所以app.locals.mongoClient是undefined),而且Jest跑完测试后连接还在后台运行,引发环境销毁错误。
修改app.js,把连接逻辑改成Promise风格,导出一个能获取已连接app的函数:
// app.js 改造后 const https = require('https'); const fs = require('fs'); const path = require('path'); const dotenv = require('dotenv'); const express = require('express'); const rateLimiter = require('express-rate-limit'); const { MongoClient } = require('mongodb'); dotenv.config(); // 这里直接加载env,不用在测试里重复处理 const createApp = async () => { const app = express(); const port = process.env.PORT || 443; const limit = rateLimiter({ window: 15 * 60 * 1000, max: 100 }); var httpsOptions; if(process.env.NODE_ENV === 'development'){ const rootCA = require('ssl-root-cas').create().addFile(path.join(__dirname, './cert/CA.pem')); https.globalAgent.options.ca = rootCA; httpsOptions = { key: fs.readFileSync(path.join(__dirname, './cert/localhost.key')), cert: fs.readFileSync(path.join(__dirname, './cert/localhost.crt')) }; } // 异步等待MongoDB连接完成 const mongoClient = await MongoClient.connect(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true }); app.locals.mongoClient = mongoClient; app.use(limit); app.use(express.json()); app.get('/', (req, res) => { res.json({path: '/'}); }); // 只有非测试环境才启动HTTP服务,测试时由Supertest处理 if (process.env.NODE_ENV !== 'test') { https.createServer(httpsOptions, app).listen(port); } return app; }; module.exports = createApp;
2. 修改测试用例,对齐测试生命周期
现在测试需要调用createApp()来获取已连接好的app,并且在测试套件结束后主动关闭MongoDB连接:
// test.js 改造后 const dotenv = require('dotenv'); const path = require('path'); dotenv.config({path: path.resolve(__dirname, '.env')}); // 直接加载env即可 const request = require('supertest'); const createApp = require('../app'); describe('GET /', () => { let app; let mongoClient; // 所有测试前初始化App和连接 beforeAll(async () => { app = await createApp(); mongoClient = app.locals.mongoClient; }); // 所有测试后关闭连接 afterAll(async () => { await mongoClient.close(); }); it('responds with path of /', async () => { const response = await request(app).get('/'); expect(response.text).toBe(JSON.stringify({path: '/'})); }); });
3. 可选优化:全局统一管理连接(多测试文件场景)
如果你的测试用例很多,不想每个测试文件都写一遍连接逻辑,可以用Jest的全局Setup/Teardown:
- 项目根目录创建
jest.setup.js:
const createApp = require('./server/app'); // 对应你的app路径 global.app = null; global.mongoClient = null; beforeAll(async () => { global.app = await createApp(); global.mongoClient = global.app.locals.mongoClient; }); afterAll(async () => { await global.mongoClient.close(); });
- 在
jest.config.js里添加配置:
module.exports = { setupFilesAfterEnv: ['./jest.setup.js'], };
这样所有测试文件都能直接使用global.app,不用重复初始化。
4. 关于“Jest did not exit...”警告
这个警告本质是测试结束后还有未关闭的异步资源(MongoDB连接、HTTP服务器),上面的方案里:
afterAll主动关闭MongoDB连接createApp里只在非测试环境启动HTTP服务器,避免额外资源占用
两者结合就能彻底解决这个警告。
内容的提问来源于stack exchange,提问作者Zack

