'Cannot find memory leak in my Express.js Jest tests

I've now spent some time trying to find memory leaks in my Jest tests, and even though I've successfully tackled some, there's still quite a lot of memory being leaked from test suite to test suite. Specifically, when I npm test (all test suites), I get the following output:

PASS  src/.../suite1.test.ts (71.154 s, 163 MB heap size)
PASS  src/.../suite2.test.ts (59.809 s, 229 MB heap size)
PASS  src/.../suite3.test.ts (9.838 s, 231 MB heap size)
PASS  src/.../suite4.test.ts (7.696 s, 242 MB heap size)
FAIL  src/.../suite5.test.ts (251 MB heap size)
PASS  src/.../suite6.test.ts (10.825 s, 318 MB heap size)
PASS  src/.../suite7.test.ts (19.679 s, 363 MB heap size)
PASS  src/.../suite8.test.ts (14.128 s, 408 MB heap size)
PASS  src/.../suite9.test.ts (16.89 s, 452 MB heap size)

From the above output if seems like there is still something that still lives after test suites get finished. The what is what's bugging me! It also seems like a memory leak is probably shared between all the test suites, as every suite is leaving some more memory heap size than the last one.

Here is the shared test structure of all my test suites:

import request from "supertest";
import app from "../../app";
import mongoose from "mongoose";
...
some models that I need for testing
...
import faker from "faker";
import eventEmitter from "../../loaders/eventEmitter";

jest.mock("../../some-module");
jest.mock("../../another-module");
jest.mock("axios", () => {
  // Require the original module to not be mocked...
  const originalModule = jest.requireActual("axios");

  return {
    ...originalModule,
    post: jest.fn().mockReturnValue({ data: { access_token: 123, expires_in: 600000 } })
  };
});

describe("TestSuite1", () => {
  beforeEach(() => jest.clearAllMocks());
  beforeAll(async () => await connectDb("TestSuite1"));
  afterEach(async () => await clearDb()); // Function that deletes collections from Mongo
  afterAll(async () => {
    await mongoose.connection.dropDatabase();
    await mongoose.connection.close();
  });

  describe("POST /some-endpoint") {
    beforeEach(async () => {
      myDocument = await buildDocument(); // Builds and saves() to Mongo some document
    });

    it("some assertion", async () => {
      const response = await request(app)
        .post("some-endpoint")
        .send(some-data)
        .set("Accept", "application/json");

      expect(response.status).toEqual(204);
      // more assertions on the response
    });
  }

Does anything seem obvious here? I can add more code if needed.

Edit: When I run tests with --detectOpenHandles, I also get this error; could it be relevant?

  ●  TCPSERVERWRAP

      108 |         it("some assertion", async () => {
      109 |           const response = await request(app)
    > 110 |             .post("some-endpoint")
          |              ^
      111 |             .send({})
      112 |             .set("some-header", header-value)
      113 |             .set("Accept", "application/json");

Edit 2: I made an attempt at closing my server after my test suite, by adding the following:

let server: http.Server;
let agent: SuperAgentTest;

beforeAll(async () => {
  await connectDb("TestSuite1");
  server = app.listen(4000, () => {
    agent = request.agent(server);
  });
});
afterAll(async () => {
  await closeDb();
  jest.resetAllMocks();
  server && server.close();
});

This seems to have fixed the open handle issue, but not the memory leaking.

Also, for clarity, the eventEmitter module that gets imported does the following:

const eventEmitter = new EventEmitter();
export default eventEmitter;


Solution 1:[1]

Let's try to analyze things a little bit:

  • You're seeing heap size increase over time.
  • The only things that happen before and after tests that are potentially leaky are connecting to a database, but you're killing the connection at the end so that's not the issue.
  • You mentioned in the comments how you handle buildDocument and it doesn't seem to affect the leak itself.

Looking at your code, I couldn't identify an issue right away (it's something I've also ran into a long time ago but didn't dig into too deply).

I thought about it a little more and in my opinion, the only other possibility is - it's not your fault and could be an issue with jest's garbage collection.

There has been discussion around this in https://github.com/facebook/jest/issues/7874.

I'd recommend running the test with --runInBand to see stats with/without it (i.e. jest --logHeapUsage --runInBand).

Additionally, as seen in the workaround in the issue above, try exposing the garbage collector with --expose-gc and adding

afterAll(() => {
  global.gc && global.gc()
})

to the common test setup to ensure garbage collection is forced.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Azarro