Jest 测试套件无法运行:“分段错误(核心转储)”或“Jest 工作进程 (pid=xxx) 被另一个进程终止”

Osc*_*anz 5 redis node.js express typescript jestjs

我正在创建一个 Express API,一切都很顺利,直到我开始遇到一些测试问题。我使用 Jest 和 Supertest 来测试端点,但我不知道为什么在运行测试时开始出现此消息。

\n
 FAIL  src/modules/ingredients/tests/updateingredient.test.ts\n  \xe2\x97\x8f Test suite failed to run\n\n    A jest worker process (pid=672269) was terminated by another process: signal=SIGSEGV, exitCode=null. Operating system logs may contain more information on why this occurred.\n\n      at ChildProcessWorker._onExit (node_modules/jest-worker/build/workers/ChildProcessWorker.js:366:23)\n\n(node:672290) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 open listeners added to [_0x323dee]. Use emitter.setMaxListeners() to increase limit\n(Use `node --trace-warnings ...` to show where the warning was created)\n
Run Code Online (Sandbox Code Playgroud)\n

我正在使用 Redis 进行缓存和速率限制,我知道它必须与其相关,但我不知道如何修复它。这些错误仅在端点测试时发生。最奇怪的是,这些错误仅在我在 VSCode 打开的情况下运行测试时显示,当我从外部终端运行测试并且 VSCode 关闭时,所有测试都会通过。

\n

我已经使用 --detectOpenHandles 运行测试来查看问题出在哪里,当我打开 VSCode 时,输出是:

\n
Segmentation fault (core dumped) \n
Run Code Online (Sandbox Code Playgroud)\n

VSCode 关闭后是:

\n
TCPWRAP\n\n       5 | const redisDb = config.get<number>('redis_db');\n       6 |\n    >  7 | export const redis = new Redis({\n         |                      ^\n       8 |   port: 6379,\n       9 |   host: '127.0.0.1',\n      10 |   db: redisDb,\n\n      at node_modules/ioredis/built/connectors/StandaloneConnector.js:44:21\n      at StandaloneConnector.connect (node_modules/ioredis/built/connectors/StandaloneConnector.js:43:16)\n      at node_modules/ioredis/built/Redis.js:121:64\n      at EventEmitter.connect (node_modules/ioredis/built/Redis.js:103:25)\n      at new Redis (node_modules/ioredis/built/Redis.js:77:18)\n      at Object.<anonymous> (src/utils/redis.ts:7:22)\n      at Object.<anonymous> (src/modules/allergens/tests/listAllergens.test.ts:5:1)\n
Run Code Online (Sandbox Code Playgroud)\n

我尝试使用 jest hooks 在所有测试之后、所有测试之前以及每次测试之后和之前以几种我发现的方式关闭 Redis 连接,但它不起作用。

\n
afterAll(async () => {\n  await new Promise((resolve) => {\n    redis.quit();\n    redis.on('end', resolve);\n  });\n});\n
Run Code Online (Sandbox Code Playgroud)\n

或者

\n
afterAll(async () => {\n  await redis.quit();\n});\n
Run Code Online (Sandbox Code Playgroud)\n

我重新安装了 VSCode、Node.js,使用了不同版本的 Node,运行 npm重建,卸载了 VSCode 的 jest 扩展,删除了 node_modules 并安装了 npm,但没有任何效果。

\n

这些是我的文件:

\n

redis.ts

\n
import Redis from 'ioredis';\nimport config from 'config';\nimport { Logger } from 'src/logger/logger';\n\nconst redisDb = config.get<number>('redis_db');\n\nexport const redis = new Redis({\n  port: 6379,\n  host: '127.0.0.1',\n  db: redisDb,\n});\n\nredis.on('ready', () => Logger.info('redis ready'));\nredis.on('error', (error) => {\n  Logger.error(`error with redis connection: ${error}`);\n  return redis.disconnect();\n});\n
Run Code Online (Sandbox Code Playgroud)\n

速率限制器.ts

\n
import { NextFunction, Request, Response } from 'express';\nimport { redis } from 'src/utils/redis';\n\nexport interface Limiter {\n  windowSize: number;\n  allowedRequests: number;\n}\nexport const rateLimiter =\n  ({ windowSize, allowedRequests }: Limiter) =>\n  async (req: Request, res: Response, next: NextFunction) => {\n    const ip = (req.headers['x-forwarded-for'] || req.socket.remoteAddress) as string;\n\n    const formatIp = (ipAddress: string) => {\n      if (ipAddress.substring(0, 7) === '::ffff:') {\n        return ipAddress.replace('::ffff:', '');\n      }\n      return ipAddress;\n    };\n    const formattedIp = formatIp(ip);\n\n    const requests = await redis.incr(formattedIp);\n\n    if (requests === 1) {\n      await redis.expire(formattedIp, windowSize);\n    } else {\n      await redis.ttl(formattedIp);\n    }\n\n    if (requests > allowedRequests) {\n      return res.status(429).send({ error: 'too many requests' });\n    }\n    return next();\n  };\n
Run Code Online (Sandbox Code Playgroud)\n

应用程序.ts

\n
import express from 'express';\nimport helmet from 'helmet';\nimport cors from 'cors';\nimport { httpLogger } from './logger/httpLogger';\nimport { Logger } from './logger/logger';\nimport { errorHandler, isTrustedError } from './error/error-handler';\nimport { apiRouter } from './routes';\n\nexport const createApp = () => {\n  const app = express();\n\n  app.use(helmet());\n  app.use(cors());\n  app.use(express.json());\n  app.use(express.urlencoded({ extended: false }));\n  app.use(httpLogger);\n\n  app.use('/api/v1', apiRouter);\n\n  app.use(errorHandler);\n\n  process.on('uncaughtException', async (error: Error) => {\n    Logger.error(error);\n    if (!isTrustedError(error)) {\n      process.exit(1);\n    }\n  });\n  process.on('unhandledRejection', (reason: string) => {\n    Logger.error(reason);\n    process.exit(1);\n  });\n\n  return app;\n};\n
Run Code Online (Sandbox Code Playgroud)\n

路线.ts

\n
import { Request, Response, Router } from 'express';\nimport { rateLimiter } from './middlewares/rateLimiter';\nimport { allergensRouter } from './modules/allergens/allergens.router';\nimport { ingredientsRouter } from './modules/ingredients/ingredients.router';\nimport { recipesRouter } from './modules/recipes/recipes.routes';\n\nexport const apiRouter = Router();\n\napiRouter.get(\n  '/health',\n\n  (_req: Request, res: Response) => {\n    const data = {\n      uptime: process.uptime(),\n      responseTime: process.hrtime(),\n      message: 'ok',\n      date: new Date(),\n    };\n    return res.status(200).send({ data });\n  }\n);\napiRouter.use(rateLimiter({ windowSize: 20, allowedRequests: 4 }));\napiRouter.use('/allergens', allergensRouter);\napiRouter.use('/ingredients', ingredientsRouter);\napiRouter.use('/recipes', recipesRouter);\n
Run Code Online (Sandbox Code Playgroud)\n

成分.router.ts

\n
import { Router } from 'express';\nimport { validate } from 'src/middlewares/validationRequest';\nimport {\n  deleteAllIngredients,\n  deleteIngredientById,\n  findIngredientById,\n  findIngredients,\n  makeIngredient,\n  patchIngredient,\n} from './ingredients.controller';\nimport {\n  createIngredientSchema,\n  deleteIngredientSchema,\n  deleteIngredientByIdSchema,\n  getIngredientByIdSchema,\n  getIngredientSchema,\n  updateIngredientSchema,\n} from './ingredients.schema';\nimport { cache } from 'src/middlewares/cache.middleware';\n\nexport const ingredientsRouter = Router();\n\ningredientsRouter.get('/', validate(getIngredientSchema), cache, findIngredients);\ningredientsRouter.get('/:id', validate(getIngredientByIdSchema), cache, findIngredientById);\n\ningredientsRouter.post('/', validate(createIngredientSchema), makeIngredient);\n\ningredientsRouter.delete('/', validate(deleteIngredientSchema), deleteAllIngredients);\ningredientsRouter.delete('/:id', validate(deleteIngredientByIdSchema), deleteIngredientById);\n\ningredientsRouter.patch('/:id', validate(updateIngredientSchema), patchIngredient);\n
Run Code Online (Sandbox Code Playgroud)\n

成分.controller.ts

\n
import { NextFunction, Request, Response } from 'express';\nimport { filterProperties } from 'src/utils/filterProperties';\nimport { isValidId } from 'src/utils/idValidation';\nimport { ApiError } from '../../error/ApiError';\nimport { redis } from 'src/utils/redis';\nimport {\n  getIngredients,\n  getIngredientById,\n  getIngredientsByAllergen,\n  createIngredient,\n  updateIngredient,\n  removeIngredientById,\n  removeAllIngredients,\n} from './ingredients.service';\n\nexport const findIngredients = async (req: Request, res: Response, next: NextFunction) => {\n  const { allergenNames } = req.query;\n\n  const properties = ['name', 'category', 'hasAllergens', 'allergens', 'allergenNames'];\n\n  const filteredQuery = filterProperties(properties, req.query);\n\n  if (allergenNames) {\n    const parsedNames = Array.isArray(allergenNames) ? allergenNames : [allergenNames.toString()];\n\n    try {\n      const foundIngredients = await getIngredientsByAllergen(parsedNames);\n\n      redis.setex(`ingredients_allergen_${parsedNames}`, 3600, JSON.stringify(foundIngredients));\n\n      return res.status(200).send({ data: foundIngredients });\n    } catch (error) {\n      return next(error);\n    }\n  }\n  try {\n    const foundIngredients = await getIngredients(filteredQuery);\n\n    Object.keys(filteredQuery).length > 0\n      ? redis.setex(\n          `ingredients_${Object.keys(filteredQuery)}`,\n          3600,\n          JSON.stringify(foundIngredients)\n        )\n      : redis.setex('ingredients', 3600, JSON.stringify(foundIngredients));\n\n    return res.status(200).send({ data: foundIngredients });\n  } catch (error) {\n    return next(error);\n  }\n};\n... more code\n
Run Code Online (Sandbox Code Playgroud)\n

listIngredients.test.ts

\n
import * as IngredientsService from '../ingredients.service';\nimport { IngredientInput } from '../ingredients.model';\nimport { createApp } from '../../../app';\nimport { redis } from 'src/utils/redis';\n\nconst app = createApp();\n\nconst ingredientInput: IngredientInput = {\n  name: 'ingredient 1',\n  category: 'eggs',\n  hasAllergens: true,\n  allergens: ['639eea5a049fc933bddebab3'],\n  allergenNames: ['celery'],\n};\n\nconst ingredientPayload = {\n  _id: '639eea5a049fc933bddebab2',\n  name: 'ingredient 1',\n  category: 'eggs',\n  hasAllergens: true,\n  allergens: ['639eea5a049fc933bddebab3'],\n  allergenNames: ['celery'],\n};\n\nconst baseApiUrl = '/api/v1/ingredients';\n\nconst getIngredientsServiceMock = jest.spyOn(IngredientsService, 'getIngredients');\n\nconst getIngredientsByAllergenServiceMock = jest.spyOn(\n  IngredientsService,\n  'getIngredientsByAllergen'\n);\n\nconst getIngredientByIdServiceMock = jest.spyOn(IngredientsService, 'getIngredientById');\n\nbeforeEach(() => {\n  jest.clearAllMocks();\n  jest.resetAllMocks();\n  redis.flushdb();\n});\n\nafterAll(async () => {\n  await redis.quit();\n});\n\n... tests start here\n
Run Code Online (Sandbox Code Playgroud)\n