Search code examples
reactjsnode.jsjestjsreact-testing-librarytesting-library

It is possible to improve even more?


I have a big React app with 4205 Jest test running in a CI and it is taking more than 40 minutes to run all the tests. This amount of time is normal?

Versions:

NPM: 7.24.0
Node: 16.10.0
@testing-library/jest-dom: 6.4.2
@testing-library/react: 12.1.5
@testing-library/user-event: 14.5.2
jest: 29.7.0
jest-canvas-mock: 2.3.1
jest-environment-jsdom: 29.4.0
jest-junit: 16.0.0
jest-sonar: 0.2.16
jest-transform-stub: 2.0.0
jsdom: 21.1.0

jest.config.js:

module.exports = {
  clearMocks: true,
  restoreMocks: true,
  moduleNameMapper: {
    //some options
  },
  setupFiles: ["<rootDir>/buildTools/define-deprecated-global.js"],
  setupFilesAfterEnv: ["<rootDir>/buildTools/setupTests.js"],
  transform: {
    //some options
  },
  transformIgnorePatterns: [`/node_modules/(?!${esModules})`],
  testEnvironment: "jsdom",
  testPathIgnorePatterns: ["/test.js"],
  testTimeout: 250000,
  coverageDirectory: "target/coverage",
  collectCoverage: true,
  coverageReporters: ["json", "lcov", "clover"],
  reporters: [
    //some entries
  ]
};

package.json script:

"test:ci": "node --max-old-space-size=8192 --experimental-vm-modules node_modules/.bin/jest --maxWorkers=20% --watchAll=false --silent",

Recently I tried to change package.json to this but the time increased:

"test:ci": "node --optimize-for-size --max-old-space-size=8192 --gc_interval=100 --concurrent-recompilation --no-compilation-cache --experimental-vm-modules node_modules/.bin/jest --maxWorkers=50% --watchAll=false --silent --ci",

Are there any recommendations to improve this? Any better configuration? Any better alternatives to Jest? I know that Node 20 has a fix for a memory leak, but I've already tried with Node 20, and the testing time is still huge.


Solution

  • Possible help:

    no-compilation-cache

    I followed this suggestion https://dev.to/retyui/resolve-memory-leaks-caused-by-jest-with-nodejs-16x-and-18x-javascript-heap-out-of-memory-3md5 and just added --no-compilation-cache to package.json but I don't see any improvement.

    Static maxWorkers

    I found --maxWorkers doesn't work very well in a CI environment. I'm requesting 9000m (9 cores) of CPU to the CI build. And with --maxWorkers=20% sometimes the tests run with 9 cores, sometimes with 6, sometimes with 4. So, if I'm requesting 9000m, probably is better to use --maxWorkers=9 or --maxWorkers=7.

    Coverage

    I read somewhere that gathering test coverage during test execution can increase test execution time. So I'm thinking about moving test coverage gathering to a different stage of CI.

    To do that, I believe, in the package.json I need to add another command and call it in the new CI stage: "test:coverage": "jest --coverage".

    And in my jest.config.js, I need to disable coverage gathering during tests execution: collectCoverage: false.

    Promises

    I have always extra care with promises because I know they can exponentially increase my tests time and they can cause multiple flaky tests. So I:

    • always try to use async-await instead of .then();
    • ensure the promise is always returned when I use .then();
    • always await for promises result. If I'm not sure if a function will return a promise or not, I check what kind of result it will return, and if is a promise, so I obviously need to await for it;
    • never use async or / and await if not necessary. I've noticed that if any async or / and await are used incorrectly in the code, it will cause some flakyness in the tests.

    To prevent some of those cases, I added these rules in my .eslintrc.json:

    "require-await": "error",
    "no-return-await": "error",
    

    Another suggestion is to use this: https://www.npmjs.com/package/eslint-plugin-promise

    A thing I'm wandering is if doing this can increase memory leaks (because I have many tests like this):

    functionToBeTested() {
      requestSomeThingToTheServer(123).then(apiResult => {
        doSomething(apiResult);
        doSomethingMore();
      });
    }
    
    test("check the Server request", (done) => {
      
      mockServerRequest((requestParams) => { //is mocking requestSomeThingToTheServer() action
        expect(requestParams).toEqual(123);
        done();
      });
      
      functionToBeTested();
    });
    

    According with this link https://betterprogramming.pub/the-4-types-of-memory-leaks-in-node-js-and-how-to-avoid-them-part-2-f21fbda5c33b:

    Promises get stored in memory and they do not clear until they are either handled or rejected

    So if I use done() in the test during the promise execution, it memory will not clear and can be a problem?

    Vitest

    I'm strongly considering migrating to Vitest. People say it's faster than Jest and doesn't seem difficult to migrate:

    React 18

    I'm not using React 18 yet but I'm concern because of this: https://github.com/testing-library/react-testing-library/issues/1235

    Seems the tests time will increase even more with React 18.

    Some extra info

    https://apidog.com/blog/jest-test-running-concurrently/ https://snird.medium.com/do-not-use-node-js-optimization-flags-blindly-3cc8dfdf76fd https://gist.github.com/stormwild/4bd3c1ec50ed055a363012a403b16365 https://www.npmjs.com/package/eslint-plugin-promise