node.js - Jest-testsvit kunde inte köras: 'Segmenteringsfel (core dump)' eller 'En jest-arbetarprocess (pid=xxx) avslutades av en annan process'


Jag skapar ett Express-API och allt gick bra tills jag började få problem med testerna. Jag använder Jest och Supertest för att testa endpoints och jag vet inte varför jag började få detta meddelande när jag kör testerna.

 FAIL  src/modules/ingredients/tests/updateingredient.test.ts
  ● Test suite failed to run

    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.

      at ChildProcessWorker._onExit (node_modules/jest-worker/build/workers/ChildProcessWorker.js:366:23)

(node:672290) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 open listeners added to [_0x323dee]. Use emitter.setMaxListeners() to increase limit
(Use `node --trace-warnings ...` to show where the warning was created)

Jag använder Redis för caching och hastighetsbegränsning och jag vet att det måste vara relaterat till det men jag vet inte hur jag ska fixa det. Dessa fel uppstår bara vid endpoint-tester. Det märkligaste är att dessa fel bara visas om jag kör tester med VSCode öppet, när jag kör tester från en extern terminal och VSCode stängd går alla tester igenom.

Jag har kört test med --detectOpenHandles för att se var problemet är och när jag har VSCode öppet är utskriften:

Segmentation fault (core dumped) 

med VSCode stängt är:

TCPWRAP

       5 | const redisDb = config.get<number>('redis_db');
       6 |
    >  7 | export const redis = new Redis({
         |                      ^
       8 |   port: 6379,
       9 |   host: '127.0.0.1',
      10 |   db: redisDb,

      at node_modules/ioredis/built/connectors/StandaloneConnector.js:44:21
      at StandaloneConnector.connect (node_modules/ioredis/built/connectors/StandaloneConnector.js:43:16)
      at node_modules/ioredis/built/Redis.js:121:64
      at EventEmitter.connect (node_modules/ioredis/built/Redis.js:103:25)
      at new Redis (node_modules/ioredis/built/Redis.js:77:18)
      at Object.<anonymous> (src/utils/redis.ts:7:22)
      at Object.<anonymous> (src/modules/allergens/tests/listAllergens.test.ts:5:1)

Jag har försökt att använda Jest-hooks för att stänga Redis-anslutningar efter alla tester, före alla tester, samt efter och före varje test på flera sätt jag har hittat, men det fungerade inte.

afterAll(async () => {
  await new Promise((resolve) => {
    redis.quit();
    redis.on('end', resolve);
  });
});

eller

afterAll(async () => {
  await redis.quit();
});

Jag har installerat om VSCode, Node.js, använt olika versioner av Node, kört npm rebuild, avinstallerat jest-tillägget i VSCode, tagit bort node_modules och kört npm install efteråt men inget fungerar.

Det här är mina filer:

redis.ts

import Redis from 'ioredis';
import config from 'config';
import { Logger } from 'src/logger/logger';

const redisDb = config.get<number>('redis_db');

export const redis = new Redis({
  port: 6379,
  host: '127.0.0.1',
  db: redisDb,
});

redis.on('ready', () => Logger.info('redis ready'));
redis.on('error', (error) => {
  Logger.error(`error with redis connection: ${error}`);
  return redis.disconnect();
});

rateLimiter.ts

import { NextFunction, Request, Response } from 'express';
import { redis } from 'src/utils/redis';

export interface Limiter {
  windowSize: number;
  allowedRequests: number;
}
export const rateLimiter =
  ({ windowSize, allowedRequests }: Limiter) =>
  async (req: Request, res: Response, next: NextFunction) => {
    const ip = (req.headers['x-forwarded-for'] || req.socket.remoteAddress) as string;

    const formatIp = (ipAddress: string) => {
      if (ipAddress.substring(0, 7) === '::ffff:') {
        return ipAddress.replace('::ffff:', '');
      }
      return ipAddress;
    };
    const formattedIp = formatIp(ip);

    const requests = await redis.incr(formattedIp);

    if (requests === 1) {
      await redis.expire(formattedIp, windowSize);
    } else {
      await redis.ttl(formattedIp);
    }

    if (requests > allowedRequests) {
      return res.status(429).send({ error: 'too many requests' });
    }
    return next();
  };

app.ts

import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import { httpLogger } from './logger/httpLogger';
import { Logger } from './logger/logger';
import { errorHandler, isTrustedError } from './error/error-handler';
import { apiRouter } from './routes';

export const createApp = () => {
  const app = express();

  app.use(helmet());
  app.use(cors());
  app.use(express.json());
  app.use(express.urlencoded({ extended: false }));
  app.use(httpLogger);

  app.use('/api/v1', apiRouter);

  app.use(errorHandler);

  process.on('uncaughtException', async (error: Error) => {
    Logger.error(error);
    if (!isTrustedError(error)) {
      process.exit(1);
    }
  });
  process.on('unhandledRejection', (reason: string) => {
    Logger.error(reason);
    process.exit(1);
  });

  return app;
};

routes.ts

import { Request, Response, Router } from 'express';
import { rateLimiter } from './middlewares/rateLimiter';
import { allergensRouter } from './modules/allergens/allergens.router';
import { ingredientsRouter } from './modules/ingredients/ingredients.router';
import { recipesRouter } from './modules/recipes/recipes.routes';

export const apiRouter = Router();

apiRouter.get(
  '/health',

  (_req: Request, res: Response) => {
    const data = {
      uptime: process.uptime(),
      responseTime: process.hrtime(),
      message: 'ok',
      date: new Date(),
    };
    return res.status(200).send({ data });
  }
);
apiRouter.use(rateLimiter({ windowSize: 20, allowedRequests: 4 }));
apiRouter.use('/allergens', allergensRouter);
apiRouter.use('/ingredients', ingredientsRouter);
apiRouter.use('/recipes', recipesRouter);

ingredients.router.ts

import { Router } from 'express';
import { validate } from 'src/middlewares/validationRequest';
import {
  deleteAllIngredients,
  deleteIngredientById,
  findIngredientById,
  findIngredients,
  makeIngredient,
  patchIngredient,
} from './ingredients.controller';
import {
  createIngredientSchema,
  deleteIngredientSchema,
  deleteIngredientByIdSchema,
  getIngredientByIdSchema,
  getIngredientSchema,
  updateIngredientSchema,
} from './ingredients.schema';
import { cache } from 'src/middlewares/cache.middleware';

export const ingredientsRouter = Router();

ingredientsRouter.get('/', validate(getIngredientSchema), cache, findIngredients);
ingredientsRouter.get('/:id', validate(getIngredientByIdSchema), cache, findIngredientById);

ingredientsRouter.post('/', validate(createIngredientSchema), makeIngredient);

ingredientsRouter.delete('/', validate(deleteIngredientSchema), deleteAllIngredients);
ingredientsRouter.delete('/:id', validate(deleteIngredientByIdSchema), deleteIngredientById);

ingredientsRouter.patch('/:id', validate(updateIngredientSchema), patchIngredient);

ingredients.controller.ts

import { NextFunction, Request, Response } from 'express';
import { filterProperties } from 'src/utils/filterProperties';
import { isValidId } from 'src/utils/idValidation';
import { ApiError } from '../../error/ApiError';
import { redis } from 'src/utils/redis';
import {
  getIngredients,
  getIngredientById,
  getIngredientsByAllergen,
  createIngredient,
  updateIngredient,
  removeIngredientById,
  removeAllIngredients,
} from './ingredients.service';

export const findIngredients = async (req: Request, res: Response, next: NextFunction) => {
  const { allergenNames } = req.query;

  const properties = ['name', 'category', 'hasAllergens', 'allergens', 'allergenNames'];

  const filteredQuery = filterProperties(properties, req.query);

  if (allergenNames) {
    const parsedNames = Array.isArray(allergenNames) ? allergenNames : [allergenNames.toString()];

    try {
      const foundIngredients = await getIngredientsByAllergen(parsedNames);

      redis.setex(`ingredients_allergen_${parsedNames}`, 3600, JSON.stringify(foundIngredients));

      return res.status(200).send({ data: foundIngredients });
    } catch (error) {
      return next(error);
    }
  }
  try {
    const foundIngredients = await getIngredients(filteredQuery);

    Object.keys(filteredQuery).length > 0
      ? redis.setex(
          `ingredients_${Object.keys(filteredQuery)}`,
          3600,
          JSON.stringify(foundIngredients)
        )
      : redis.setex('ingredients', 3600, JSON.stringify(foundIngredients));

    return res.status(200).send({ data: foundIngredients });
  } catch (error) {
    return next(error);
  }
};
... more code

listIngredients.test.ts

import * as IngredientsService from '../ingredients.service';
import { IngredientInput } from '../ingredients.model';
import { createApp } from '../../../app';
import { redis } from 'src/utils/redis';

const app = createApp();

const ingredientInput: IngredientInput = {
  name: 'ingredient 1',
  category: 'eggs',
  hasAllergens: true,
  allergens: ['639eea5a049fc933bddebab3'],
  allergenNames: ['celery'],
};

const ingredientPayload = {
  _id: '639eea5a049fc933bddebab2',
  name: 'ingredient 1',
  category: 'eggs',
  hasAllergens: true,
  allergens: ['639eea5a049fc933bddebab3'],
  allergenNames: ['celery'],
};

const baseApiUrl = '/api/v1/ingredients';

const getIngredientsServiceMock = jest.spyOn(IngredientsService, 'getIngredients');

const getIngredientsByAllergenServiceMock = jest.spyOn(
  IngredientsService,
  'getIngredientsByAllergen'
);

const getIngredientByIdServiceMock = jest.spyOn(IngredientsService, 'getIngredientById');

beforeEach(() => {
  jest.clearAllMocks();
  jest.resetAllMocks();
  redis.flushdb();
});

afterAll(async () => {
  await redis.quit();
});

... tests start here

Relaterade artiklar