Fastify项目Jest+Supertest测试遇两问题:钩子报错与TCP句柄未关闭
Fastify + Jest + Supertest 测试问题解决指南
问题1:TypeError: Cannot read properties of undefined (reading 'length')
错误场景
测试健康检查接口时触发该错误,错误栈指向Fastify内部钩子preParsingHookRunner中的length属性读取操作。
排查与解决
该错误大概率和@fastify/csrf-protection插件的配置或路由校验规则有关:
- 核心原因:Fastify v5中,csrf保护插件默认对所有路由启用校验,当请求未携带csrf token时,插件内部逻辑可能出现未定义值访问;同时插件依赖cookie插件的签名配置,缺失配置也会导致异常。
- 解决方案:
- 给健康检查路由跳过csrf校验:在注册路由时添加配置,跳过该路由的csrf验证:
// authRoutes示例代码 import type { FastifyInstance } from 'fastify'; export default async function authRoutes(fastify: FastifyInstance) { fastify.get('/healthcheck', { config: { csrf: false } }, async (req, reply) => { return reply.send({ status: 'ok' }); }); } - 完善csrf插件配置:注册csrf插件时指定cookie相关配置,与cookie插件的签名规则匹配:
// server.ts中修改csrf插件注册逻辑 await fastify.register(cookiePlugin, { secret: process.env.COOKIE_SECRET || 'dev-secret', // 必须配置签名密钥 }); await fastify.register(csrfPlugin, { cookie: { signed: true }, }); - 测试环境按需加载插件:如果测试阶段不需要csrf保护,可根据环境变量跳过插件加载:
const buildServer = async () => { const fastify = Fastify({ logger: process.env.NODE_ENV !== 'test' }); await fastify.register(sensiblePlugin); await fastify.register(cookiePlugin, { secret: 'test-secret' }); // 非测试环境才加载csrf插件 if (process.env.NODE_ENV !== 'test') { await fastify.register(csrfPlugin); } await fastify.register(helmetPlugin); await fastify.register(authRoutes, { prefix: '/api/auth' }); return fastify; };
- 给健康检查路由跳过csrf校验:在注册路由时添加配置,跳过该路由的csrf验证:
问题2:未关闭TCP句柄导致Jest超时退出
错误场景
调用app.close()后Jest仍无法正常退出,触发测试超时提示,后台检测到未关闭的TCPWRAP句柄。
排查与解决
核心问题是Fastify实例关闭不彻底,或异步操作未等待完成:
- 核心原因:
app.close()是异步方法,未等待其执行完成就结束测试;Fastify的日志组件或第三方插件可能持有未释放的TCP连接资源。 - 解决方案:
- 等待close操作完成:在
afterAll中必须使用await等待实例关闭:afterAll(async () => { await app.close(); }); - 禁用测试环境日志:Fastify的日志组件可能持有TCP连接(如远程日志传输),测试时关闭日志可避免资源泄漏:
// buildServer中初始化Fastify时关闭测试环境日志 const fastify = Fastify({ logger: process.env.NODE_ENV !== 'test' }); - 增加超时时间:如果实例关闭需要更长时间,可给
afterAll设置超时阈值:afterAll(async () => { await app.close(); }, 10000); // 设置10秒超时 - 应急强制退出:如果以上方法无效,可在关闭实例后强制终止进程(仅应急使用):
afterAll(async () => { await app.close(); process.exit(0); });
- 等待close操作完成:在
完整测试文件示例
修改后的测试文件:
import supertest from 'supertest'; import buildServer from '../server'; describe('Server', () => { let app: Awaited<ReturnType<typeof buildServer>>; beforeAll(async () => { // 测试环境禁用日志 app = await buildServer({ logger: false }); }); afterAll(async () => { await app.close(); }, 10000); test('GET /api/auth/healthcheck', async () => { await supertest(app.server).get('/api/auth/healthcheck').expect(200); }); });
内容的提问来源于stack exchange,提问作者Nazar Duma
相关产品推荐
相关产品推荐

