Configure different Jest timeouts for unit and integration tests in the same project

nausaf

nausaf

Posted on October 17, 2022

Configure different Jest timeouts for unit and integration tests in the same project

The Problem

  • You want to configure different test execution timeouts for Jest tests in different folders in the same project e.g. 1 second for tests in ./tests/unitTests/ and 60 seconds for tests in ./tests/integrationTests.

  • During debugging, the timeout for tests in any folder should be quite large, say 10 minutes.

    Otherwise Jest could throw timeout errors like thrown: "Exceeded timeout of 5000 ms for a test. even if a test completes successfully. This happens when debugging a test takes longer than the (default or explicitly configured) timeout for it.

    Image description

Solution

Given the folder structure:

.
│   jest.config.js
│   package-lock.json
│   package.json
│
└───tests
    ├───integrationTests
    │       integrationTestSuite.test.js
    │
    └───unitTests
            unitTestSuite.test.js
Enter fullscreen mode Exit fullscreen mode

a solution is as follows:

  1. npm install --save-dev debugger-is-attached

  2. Create a jestSetup.js file in each of the test folders and set the desired timeout via a call to jest.setTimeout() method.
    So we have a tests/unitTests/jestSetup.js file:

    jest.setTimeout(1000); //timeout of 1 second
    

    and a tests/integrationTests/jestSetup.js file:

    jest.setTimeout(60000); //timeout of 1 minute
    
  3. Create jest.config.js in project root as follows

    const { debuggerIsAttached } = require("debugger-is-attached");
    const path = require("path");
    
    module.exports = async () => {
      const isDebuggerAttached = await debuggerIsAttached();
    
      const unitTestFolder = "<rootDir>/tests/unitTests";
      const integrationTestFolder = "<rootDir>/tests/integrationTests";
      const getSetupFiles = (folder) =>
        isDebuggerAttached ? [] : [path.join(folder, "jestSetup.js")];
    
      const baseProjectConfig = {
        //Here put any properties that are the same for
        //all folders and can be specified at level
        //of the project object (all such properties
        //are declared in type ProjectConfig in
        //Config.ts in Jest repo)
      };
    
      let config = {
        //any config key/values in configuration (except those
        //that are to specified in ProjectConfig)
        //e.g. 
        //collectCoverage: true,
    
        //project config
        projects: [
          {
            ...baseProjectConfig,
            displayName: "UnitTests",
            testMatch: [path.join(unitTestFolder, "**/*.test.js")],
            slowTestThreshold: 1000, //1 second
            setupFilesAfterEnv: getSetupFiles(unitTestFolder),
          },
          {
            ...baseProjectConfig,
            displayName: "IntegrationTests",
            testMatch: [path.join(integrationTestFolder, "**/*.test.js")],
            slowTestThreshold: 60000, //1 minute
            setupFilesAfterEnv: getSetupFiles(integrationTestFolder),
          },
        ],
        //any other Jest config goes here
        //(these are any properties declared in type
        //InitialConfig but not in type ProjectConfig
        //in Config.ts in Jest repo)
      };
    
      if (isDebuggerAttached) config["testTimeout"] = 600000; //ten minutes
    
      return config;
    };
    
    
  4. In User Settings (settings.json which appears when from the Ctrl + P command pallette you select Preferences: Open User Setting (JSON)), set "jest.monitorLongRun" to a value that is equal to or greater than the largest of the folder-specific timeouts declared in jest.config.js above. For example,

    "jest.monitorLongRun": 60000, //1 minute
    

Explanation

Configuring different timeouts for different test folders

testTimeout property can be set in jest.config.js in project root to set a timeout other than the default of 5s:

module.exports = {
    ...
    testTimeout: 60000; //60 seconds
};
Enter fullscreen mode Exit fullscreen mode

How to specify testTimeout separately for different subfolders?

The canonical way to assign different configurations to tests in different subfolders is to use Jest's monorepo configuration. We pretend, as far as Jest is concerned, that folders tests/integrationTests and tests/unitTests are two separate projects in a monorepo (a collection of related projects stored in a single repository).

Using this approach we can configure separate timeouts for our two subfolders as follows:

  1. Create a jest.config.js in each subfolder. In this set testTimeout property as shown in the snippet above.
  2. Declare the different folder-specific config files as projects in the top-level jest.config.js:

    module.exports = {
      projects: [
        "<rootDir>/tests/unitTests/jest.config.js",
        "<rootDir>/tests/integrationTests/jest.config.js",
      ],
    };
    

At this point the folder structure would be as follows:

.
│   jest.config.js
│   package-lock.json
│   package.json
│
└───tests
    ├───integrationTests
    │       integrationTestSuite.test.js
    │       jest.config.js
    │
    └───unitTests
            jest.config.js
            unitTestSuite.test.js
Enter fullscreen mode Exit fullscreen mode

The problem is that if we configure testTimeout property in a folder-specific config file it will not have any effect.

This is because testTimeout is not defined in type ProjectConfig in Config.ts which specifies the config schema of project (which for use are the two subfolders of tests).

Even though the top-level as well as folder-specific config files are all called jest.config.js, the properties that are allowed in folder level config are not all the same as the properties allowed in top level config.

Folder-specific config files need to have be those defined in type ProjectConfig whereas top-level jest.config.js specifies properties that are probably declared in type InitialConfig in Config.ts.

Many keys are defined in both types but not testTimeout: it is only contained in InitialConfig and therefore only has effect if declared in the top-level config file.

Therefore testTimeout property cannot be use to override test timeouts in jest.config.js files in subfolders.

Instead, to set timeout at subfolder level:

  1. We can call jest.setTimeout(TIMEOUT_IN_MS) in a .js file in the subfolder, conventionally named jestSetup.js.
  2. Declare jestSetup.js in the folder-level jest.config.js so that it would be run by Jest before any tests in that folder are executed.

For example create tests/integrationTests/jestSetup.js as follows:

jest.setTimeout(60000); //timeout of 1 minute
Enter fullscreen mode Exit fullscreen mode

and a tests/integrationTests/jest.config.js to go with it:

module.exports = {
  displayName: "IntegrationTests",
  setupFilesAfterEnv: [`<rootDir>/jestSetup.js`],
};
Enter fullscreen mode Exit fullscreen mode

For unit tests, ./tests/unitTests/jest.config.js would be the same as the config file for integration tests folder shown above (because <rootDir> always resolves to the containing folder). However ./tests/integrationTests/jestSetup.js would specify a different timeout:

jest.setTimeout(1000); //timeout of 1 second
Enter fullscreen mode Exit fullscreen mode

The folder structure of this working solution looks like this:

.
│   jest.config.js
│   package-lock.json
│   package.json
│
└───tests
    ├───integrationTests
    │       integrationTestSuite.test.js
    │       jest.config.js
    |       jestSetup.js
    │
    └───unitTests                 
            jest.config.js
            jestSetup.js
            unitTestSuite.test.js
Enter fullscreen mode Exit fullscreen mode

We can eliminate folder-specific jest.config.js files by pulling the info in the top level config so the ./jest.config.js in the root now looks like this:

module.exports = {
  projects: [
    {
      displayName: "UnitTests",      
      testMatch: ["<rootDir>/tests/unitTests/**/*.test.js"],
      setupFilesAfterEnv: ["<rootDir>/tests/unitTests/jestSetup.js"],
    },
    {
      displayName: "IntegrationTests",
      testMatch: ["<rootDir>/tests/integrationTests/**/*.test.js"],
      setupFilesAfterEnv: ["<rootDir>/tests/integrationTests/jestSetup.js"],
    },
  ],
};

Enter fullscreen mode Exit fullscreen mode

The folder structure of this more compact solution looks like this:

.
│   jest.config.js
│   package-lock.json
│   package.json
│
└───tests
    ├───integrationTests
    │       integrationTestSuite.test.js
    │       jestSetup.js
    │
    └───unitTests
            jestSetup.js
            unitTestSuite.test.js
Enter fullscreen mode Exit fullscreen mode

I'd like to point out three improvements that can be made:

  1. It is worth adding a slowTestThreshold property in every project object and set it equal to or somewhat greater than the timeout declared in the corresponding jestSetup.js.

    A longer timeout prevents Jest from throwing an error on longer running tests. But it would still show the execution time of such a tests with a red background:

    Image description

    Adding slowTestThreshold: 60000 to the folder's config in jest.config.js tells Jest that this is how long you expect tests in that folder to take so it doesn't show execution times in warning colour.

  2. If a property is exported both ProjectConfig and InitialConfig types then, if you configure it in top level jest.config.js but not in folder-level config, it would be overridden by project config to its default value which would probably be null or undefined.

    For example given a jest.config.js that looks like this:

    module.exports = {
        setupFiles = ['./topLevelSetupFile.js'],
        ...
    
        projects: [
            {
                displayName: "IntegrationTests",
                testMatch: ["<rootDir>/tests/integrationTests/**/*.test.js"],
                setupFilesAfterEnv: ["<rootDir>/tests/integrationTests/jestSetup.js"],
            }
        ];
    };
    

    In the effective folder-specific configuration for folder ./tests/integrationTests/, setupFiles property would be set to nothing (null I think).

    A nice solution (from Orlando Bayo's post) is to set the shared config - properties that are shared across all folders - in a baseProjectconfig object and spread this into every project object.

Incorporating both of these improvements into our solution, we have the following jest.config.js:

const baseProjectConfig = {
    //Here put any properties that are the same for
    //all folders and can be specified at level
    //of the project object (all such properties
    //are declared in type ProjectConfig in
    //Config.ts in Jest repo)
  };

  let config = {
    //any config key/values in configuration (except those
    //that are specified in ProjectConfig)
    //e.g. 
    //collectCoverage: true,

    //project config
    projects: [
      {
        ...baseProjectConfig,
        displayName: "UnitTests",
        testMatch: [path.join(unitTestFolder, "**/*.test.js")],
        slowTestThreshold: 1000, //1 second
        setupFilesAfterEnv: getSetupFiles(unitTestFolder),
      },
      {
        ...baseProjectConfig,
        displayName: "IntegrationTests",
        testMatch: [path.join(integrationTestFolder, "**/*.test.js")],
        slowTestThreshold: 60000, //1 minute
        setupFilesAfterEnv: getSetupFiles(integrationTestFolder),
      },
    ],        
    //any other Jest config goes here
    //(these are any properties declared in type
    //InitialConfig but not in type ProjectConfig
    //in Config.ts in Jest repo)
  };

module.exports = config
Enter fullscreen mode Exit fullscreen mode
  1. Jest VS Code extension still shows a popup if a test takes too long to execute (although, because of the configuration above, the test won't fail on a timeout):

    Image description

    To prevent this warning window from popping up, set "jest.monitorLongRun" in User Setting file (to edit it select Preferences: Open User Settings from Ctrl + P command pallette) to the value of the longest of your folder-specific timeouts e.g.:

    "jest.monitorLongRun": 60000, //1 minute
    

Setting a long timeout when debugging

When debugging, if you take longer to step through a test than the (default or explicitly configured) timeout, Jest would throw a timeout error at the end even if the test completed successfully. I find that for a test to fail like this is confusing when you're debugging a test that you expect to pass.

Image description

To address this issue we can use the debugger-is-attached package. This exports an async function that returns true if debugger is attached and false otherwise.

It can be integrated into jest.config.js by exporting an async function that returns the config object instead of directly returning the object in question.

const { debuggerIsAttached } = require("debugger-is-attached");

module.exports = async () => {
    //do async things
    ...
    return {
        //the config object
    }
}
Enter fullscreen mode Exit fullscreen mode

Inside the function, if the debugger is not attached then everything should be the same as before but if it is attached, then we make two changes to the returned object:

  • not configure jestSetup.js, the file that declares the timeout for tests in its containing folder, to run.

  • set testTimeout property to 10 minutes (600000 ms) to give us plenty of time to debug a test.

After these changes, the final top-level jest.config.js is as shown in TL;DR at the top.

Thanks for reading. Any comments or suggestions for improvement would be greatly appreciated.

💖 💪 🙅 🚩
nausaf
nausaf

Posted on October 17, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related