Configure different Jest timeouts for unit and integration tests in the same project
Tags: jest, node, javascript, testing
Published: 2022-10-17 21:54:21


# TL;DR

* We 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5pocjkvs6vvv6krd1nom.png)    

Given the folder structure:

```
.
│   jest.config.js
│   package-lock.json
│   package.json
│
└───tests
    ├───integrationTests
    │       integrationTestSuite.test.js
    │
    └───unitTests
            unitTestSuite.test.js
```

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:

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

    and a `tests/integrationTests/jestSetup.js` file:
    ```javascript
    jest.setTimeout(60000); //timeout of 1 minute
    ```



3. <a id="complete-config"></a>Create `jest.config.js` in project root as follows
    
    ```javascript
    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
    ```


# Discussion



### 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:

```javascript
module.exports = {
    ...
    testTimeout: 60000; //60 seconds
};
```

**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](https://www.atlassian.com/git/tutorials/monorepos) (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`**:

    ```javascript
    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
```

**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`](https://github.com/facebook/jest/blob/main/packages/jest-types/src/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`](https://github.com/facebook/jest/issues/9529). 

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](https://jestjs.io/docs/configuration#setupfilesafterenv-array). 

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

```javascript
jest.setTimeout(60000); //timeout of 1 minute
```

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

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

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:

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

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
```

**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:

```javascript
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"],
    },
  ],
};

```

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
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sd0xi7z77bf8abyi4hrv.png)

    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](https://github.com/facebook/jest/blob/main/packages/jest-types/src/Config.ts#L424) 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:

    ```javascript
    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](https://orlandobayo.com/blog/monorepo-testing-using-jest/)) 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`**:

```javascript
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
```

3. 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k5crv5o8pzxb9ztejo6k.png)

    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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5pocjkvs6vvv6krd1nom.png)

To address this issue we can use the [`debugger-is-attached`](https://www.npmjs.com/package/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.

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

module.exports = async () => {
    //do async things
    ...
    return {
        //the config object
    }
}
```

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](#complete-config). 

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