Jest can be configured to run tests across multiple packages within a monorepo, allowing each package to have its own specific test setup.
Let’s see this in action. Imagine a monorepo with two packages, service-a and service-b.
monorepo/
├── package.json
├── packages/
│ ├── service-a/
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── index.js
│ │ └── jest.config.js
│ └── service-b/
│ ├── package.json
│ ├── src/
│ │ └── index.js
│ └── jest.config.js
└── jest.config.js
In this setup, we can have a root jest.config.js and individual jest.config.js files within each package. The root configuration often handles global settings like test runners, reporters, and can even include or merge configurations from sub-packages.
Root jest.config.js:
// monorepo/jest.config.js
module.exports = {
projects: [
'<rootDir>/packages/service-a/jest.config.js',
'<rootDir>/packages/service-b/jest.config.js',
],
// Global settings that apply to all projects
testEnvironment: 'node',
verbose: true,
};
This root configuration tells Jest to discover and run tests defined by the configurations found in packages/service-a and packages/service-b. The projects array is key here, allowing Jest to treat each entry as a separate Jest project, each with its own configuration.
service-a/jest.config.js:
// monorepo/packages/service-a/jest.config.js
module.exports = {
displayName: 'service-a',
rootDir: '../../', // Root directory of the monorepo
roots: ['<rootDir>/packages/service-a/src'],
testMatch: ['<rootDir>/packages/service-a/src/**/*.test.js'],
moduleNameMapper: {
'^@service-a/(.*)$': '<rootDir>/packages/service-a/src/$1',
},
transform: {
'^.+\\.js$': 'babel-jest',
},
// Package-specific settings
setupFilesAfterEnv: ['<rootDir>/packages/service-a/jest.setup.js'],
coverageDirectory: '<rootDir>/coverage/service-a',
};
Here, service-a has specific configurations: a display name for clear output, a different rootDir to correctly resolve paths relative to the monorepo root, specific roots and testMatch patterns for its tests, a moduleNameMapper for its internal aliases, and a setupFilesAfterEnv script for its unique test setup. The displayName is particularly useful in monorepos to differentiate test output from each package.
service-b/jest.config.js:
// monorepo/packages/service-b/jest.config.js
module.exports = {
displayName: 'service-b',
rootDir: '../../',
roots: ['<rootDir>/packages/service-b/src'],
testMatch: ['<rootDir>/packages/service-b/src/**/*.spec.js'], // Different file extension
transform: {
'^.+\\.jsx$': 'babel-jest', // Expecting JSX files
},
// Package-specific settings
testEnvironment: 'jsdom', // Different environment for UI testing
moduleFileExtensions: ['js', 'jsx'], // Including jsx
};
service-b showcases further customization: it uses .spec.js files for tests, transforms .jsx files, and importantly, runs in a jsdom environment, distinct from service-a’s node environment. This demonstrates how each package can tailor its testing environment and file matching to its specific needs.
When you run npm test or yarn test at the monorepo root, Jest, guided by the root jest.config.js, will orchestrate the execution of tests for both service-a and service-b according to their individual configurations. You’ll see output clearly labeled by their displayNames.
The rootDir setting in each sub-package’s Jest config is crucial. Setting it to '../../' (relative to the sub-package’s config file) correctly points Jest to the monorepo’s root directory. This ensures that all other path-based configurations like roots, testMatch, coverageDirectory, and setupFilesAfterEnv are resolved correctly from the monorepo’s top level, preventing issues with relative pathing.
When Jest encounters a configuration that specifies projects, it doesn’t just look for a single config file; it recursively discovers other Jest configurations within the specified paths. This allows for a hierarchical or distributed configuration approach. In our monorepo, the root jest.config.js acts as the orchestrator, listing the individual project configurations. Each individual project configuration then defines its own scope and settings.
A common pattern is to use jest.config.js in each package and then have a root jest.config.js that uses the projects array to point to all the package-level configurations. This way, the root configuration can provide overarching settings (like --ci flags or global reporters) while delegating specific test environments, transformers, or setup files to each package.
The rootDir in the sub-package configurations should point to the monorepo root. This is essential for Jest to correctly resolve paths like <rootDir>/packages/service-a/src or <rootDir>/coverage/service-a. Without this, Jest would interpret <rootDir> relative to the package directory, leading to incorrect file discovery and resolution.
The interplay between the root jest.config.js and individual package configurations is managed by Jest’s "projects" feature. When Jest runs, it reads the root config, sees the projects array, and then spins up separate Jest instances for each configuration listed in that array. Each instance then applies its own configuration. This isolation is what allows service-a to use node and service-b to use jsdom simultaneously without conflict.
The displayName property in each package’s config is not just for aesthetics; it’s a fundamental way to distinguish test runs when multiple Jest projects are executing concurrently or sequentially. Without it, the output can become a jumbled mess, making it hard to track which tests belong to which package.
When you have multiple jest.config.js files, Jest merges configurations. If a setting is defined in both a parent project and a child project (e.g., a global testEnvironment at the root and a specific testEnvironment in a package), the more specific configuration (the package’s) will take precedence for that package’s tests. However, in the projects array scenario, each project is largely independent, with the root config primarily serving to discover and launch them.
The roots option in Jest specifies the directories Jest should look for test files in. By setting roots: ['<rootDir>/packages/service-a/src'] in service-a’s config, you’re telling that specific Jest project to only scan within that directory for tests, rather than searching the entire monorepo. This significantly speeds up test discovery and prevents accidental inclusion of tests from other packages.
The testMatch option provides a glob pattern to find test files. Using testMatch: ['<rootDir>/packages/service-a/src/**/*.test.js'] ensures that only files ending with .test.js within service-a’s source directory are considered tests for that project. This allows service-b to use a different pattern like **/*.spec.js without conflict.
Next, you’ll want to explore how to manage shared Jest configurations across multiple packages within the monorepo.