Node.js实验性Testrunner启动Express服务器时冻结,如何解决?
我正在使用Node v18的实验性Testrunner,将Express作为开发依赖用于HTTP集成测试,大部分测试正常,但有一个测试会导致Testrunner冻结/停止运行(无法继续)。我使用TS开发,但该问题在JS环境中也可复现,测试文件HttpTests.js内容如下:
import assert from 'assert/strict'; import express from 'express'; import test from 'node:test'; test('Http', async () => { const server = express(); server.listen(3000); assert.ok(false); });
通过npm脚本"test": "node --test $(find . -name '*Tests.js')"运行时,Testrunner会出现异常。请问问题出在哪里,或者缺少了什么配置?
为什么不使用默认的执行模型?
由于我使用TS,必须找到让ts-node与Testrunner配合的方法,目前我的TS项目使用特定npm脚本运行正常。
复现步骤
- 我创建了包含TS和JS版本的最小复现仓库
- 手动复现可执行:
- 执行命令:
mkdir reproduction && cd reproduction && npm init -y && npm install express - 创建test目录及其中的HttpTests.js文件,内容如上
- 修改package.json为:
{ "name": "reproduction", "type": "module", "scripts": { "test": "node --test $(find . -name '*Tests.js')" } } - 运行测试脚本,Testrunner将无法正常结束。
- 执行命令:
已知Testrunner仍处于实验阶段
我清楚这一点,但项目中大部分测试运行正常,示例代码如下:
await t.test('subtest - saves data.', async () => { const expectedResult = {}; const api = express(); const port = await getRandomPort(); const server = api .use(express.json()) .post('/save', (request, response) => { response.json(expectedResult); }) .listen(port); const httpDataProvider = new HttpDataProvider({ url: `http://localhost:${port}` }); const actualResult = await httpDataProvider.saveSomething(); assert.deepEqual(actualResult, expectedResult); server.close(); });
问题原因
核心问题是测试断言失败后,Express服务器未被关闭,导致Node.js进程因存在活跃的网络监听资源而无法退出。
在出问题的测试代码中,server.listen(3000)启动了服务器,但assert.ok(false)会直接抛出错误,后续关闭服务器的逻辑根本没机会执行。Node.js的Testrunner会等待所有异步资源(包括网络监听)释放后才会终止进程,因此服务器的监听进程会一直占用资源,导致Testrunner冻结。
解决方法
1. 用try/finally保证服务器必被关闭
无论测试成功还是失败,都要确保服务器被关闭,修改后的测试代码如下:
import assert from 'assert/strict'; import express from 'express'; import test from 'node:test'; test('Http', async () => { const server = express(); server.listen(3000); try { assert.ok(false); } finally { // 无论断言是否失败,都会执行关闭逻辑 server.close(); } });
2. 搭配随机端口使用(推荐)
像你正常运行的测试那样,使用随机端口启动服务器,既可以避免端口占用冲突,也能让测试更健壮:
import assert from 'assert/strict'; import express from 'express'; import test from 'node:test'; import { createServer } from 'node:net'; // 辅助函数:获取随机可用端口 async function getRandomPort() { return new Promise((resolve, reject) => { const server = createServer(); server.listen(0, () => { const port = server.address().port; server.close(() => resolve(port)); }); server.on('error', reject); }); } test('Http', async () => { const server = express(); const port = await getRandomPort(); server.listen(port); try { assert.ok(false); } finally { server.close(); } });
3. 可选:监听服务器启动事件
如果需要确保服务器完全启动后再执行测试逻辑,可以监听listening事件:
test('Http', async () => { const server = express(); await new Promise(resolve => server.listen(3000, resolve)); try { assert.ok(false); } finally { server.close(); } });
内容的提问来源于stack exchange,提问作者baitendbidz

