How do I run Jest in CI and split the tests with --shard?
A good Jest setup for CI runs the project’s own Jest on a supported Node version. It runs Jest in CI mode, so that a new snapshot fails the run. It lets you rerun one test file, and it splits a large suite across machines with jest --shard. You can meet each requirement with the Jest CLI and a CI job. Dagger’s official Jest module meets the same requirements in one place, and runs the same check on a laptop and in CI.
What a good Jest CI setup needs
Jest’s own docs give four requirements. This guide uses them to judge each approach, Dagger included:
- Use the project’s own Jest on a supported Node version. The Jest docs install Jest as a dev dependency with
npm install --save-dev jest(Getting Started). Jest 30 says: “The minimum supported Node versions are now 18.x” (Upgrading to Jest 30). - Run Jest in CI mode, so that new snapshots fail. With
--ci, a new snapshot does not pass. Jest “will fail the test and require Jest to be run with--updateSnapshot.” The snapshot docs give the reason: “since new snapshots automatically pass, they should not pass a test run on a CI system”. - Rerun one test file.
jest path/to/my-test.jsruns only the tests in files that match the argument (Jest CLI). A developer can then repeat one CI failure without the full suite. - Split a large suite across machines. Jest’s
--shardoption runs one part of the suite on each machine.
Run Jest by hand in a CI job
The Jest CLI meets each requirement. Install jest as a dev dependency, then run the suite in CI mode from the project root:
npx jest --ci
You can usually leave out --ci in CI. Jest sets its ci option from the environment by default (Jest source). GitHub Actions sets CI to true in every job (variables reference). To add a snapshot, run npx jest -u on your machine and commit the snapshot file. To rerun one file, give its path: npx jest src/cart.test.js.
In CI, a job checks out the code and installs Node with actions/setup-node or the equivalent of the CI system. Then it installs the dependencies and runs Jest.
To split the suite, run one shard on each machine. Both numbers start at 1. This guide’s project has three test files, split into two shards:
npx jest --ci --shard=1/2
npx jest --ci --shard=2/2
$ npx jest --ci --shard=1/2
PASS src/sum.test.js
PASS src/receipt.test.js
Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 1 passed, 1 total
Time: 0.42 s
Ran all test suites.
$ npx jest --ci --shard=2/2
PASS src/cart.test.js
✓ total of an empty cart is 0 (3 ms)
✓ total multiplies price and quantity
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.206 s
Ran all test suites.
In GitHub Actions, a matrix runs one job for each shard index. The Jest docs say: “When shard is specified the configured testSequencer has to implement a shard method.”
The default sequencer sorts test files by a SHA-1 hash of their paths. Then it gives each shard about the same number of files. It does not use test durations, so one shard can get more slow files than the others. To balance shards in a different way, set testSequencer to a class that extends @jest/test-sequencer. Its shard(tests, { shardIndex, shardCount }) method returns the files for one shard. The Jest docs say that sharding happens before sorting.
Where the hand-built setup strains
The commands are short, but each requirement lives in a different place:
- The Node version is in two places. The CI job sets
node-version, and each laptop has its own Node. They can drift apart, and Jest then runs on a Node version that the project did not test. - The shards are a fixed count of files. The matrix fixes the shard count, and the default sequencer splits by file count, not by duration. A balanced split needs a custom sequencer that you write and maintain.
- The matrix is CI plumbing. A move to another CI system means a rewrite of the shard jobs in the syntax of that system.
- A CI failure is hard to repeat. To rerun a failed file as CI ran it, a developer must match the Node version and the install of the CI job by hand.
Meet them with the Dagger Jest module
The Dagger Jest module meets the same four requirements, with the Node image, the install, and the Jest command defined in one place. A Dagger module is a package of functions that test, lint, or build a project. The Dagger Engine runs those functions in containers, on a laptop or in CI.
Complete the Quickstart in the repository first. Then install the module from the project root:
dagger module install dagger.io/js/jest
dagger.io/js/jest ➡️ https://github.com/dagger/jest#v0.3.0
Installed module "jest" in /work/dagger.toml
The command adds the module to dagger.toml, the workspace configuration for everyone who uses the repository:
[modules.jest]
source = "dagger.io/js/jest"
The command also writes dagger.lock, which pins the module to an exact Git commit. Commit both files.
The module treats each directory with a jest.config.js, .mjs, .cjs, .ts, .mts, .cts, or .json file as a Jest project. A jest key in package.json does not mark a project. The module’s README gives the reason: every npm package has a package.json.
A check is a function that passes or fails, such as a test run. The Jest module has one check, test, and runs it once for each Jest project. Add --all to the list command to see one row for each test file:
dagger check -l --all
MODULE JEST-PROJECT JEST-TEST-FILE CHECK DESCRIPTION
jest web src/cart.test.js test Run Jest tests.
jest web src/receipt.test.js test Run Jest tests.
jest web src/sum.test.js test Run Jest tests.
The next sections show how the module meets each requirement.
Use the project’s own Jest on a supported Node version
The module installs the project’s dependencies and runs the project’s own Jest. Run the tests:
dagger check
== CHECKS == ✔ 1 passed
✔ dag://?check=test&jest-project=web 10.2s OK
== TESTS ==
✔ src/cart.test.js 2 passed
✔ src/receipt.test.js 1 passed
✔ src/sum.test.js 1 passed
✔ 4 passed
When all the files of a project are selected, the module runs jest with the project’s config. Jest’s own testMatch and other options then decide what runs. The Jest config does not change.
The module installs dependencies in a separate step. That step reads only the package files and lockfiles. A change to a source file or test file then does not start a new install (README). In this guide’s project, the first dagger check took 67.7 seconds, with the npm install. The output above comes from a later run, with the install already in the cache. A rerun with no changes took 8.8 seconds. A rerun after one new test in a test file took 10.9 seconds. These numbers come from one run on one machine.
The tests run in node:25-alpine by default. If the project uses a different Node release, set the baseImageAddress setting to that release:
dagger module settings jest baseImageAddress node:22-alpine
The setting goes into dagger.toml, so every machine uses the same Node image:
[modules.jest]
source = "dagger.io/js/jest"
[modules.jest.settings]
baseImageAddress = "node:22-alpine"
A setting applies to every Jest project in the workspace. The module reference lists the install settings, such as packageManager and installFlags.
Run Jest in CI mode, so that new snapshots fail
The module sets CI to true in its container (module source), so Jest runs in CI mode with no flag. A new snapshot then fails the check on a laptop too. In this run, src/receipt.test.js had a snapshot assertion and no snapshot file:
✘ dag://?check=test&jest-project=web 4.2s ERROR
== TESTS ==
✘ src/receipt.test.js FAIL
✘ src/receipt.test.js › receipt FAIL
Error: expect(received).toMatchSnapshot()
Snapshot name: `receipt 1`
New snapshot was not written. The update flag must be explicitly passed to write a new snapshot.
This is likely because this test is run in a continuous integration (CI) environment in which snapshots are not written by default.
Write the snapshot with npx jest -u on your machine, review it, and commit it, as the Jest docs recommend.
Rerun one test file
Select a project with --jest-project and its directory, relative to the workspace root. Select a test file with --jest-test-file and its path, relative to the project. Repeat --jest-test-file to select more files:
dagger check --jest-project=web --jest-test-file=src/cart.test.js --jest-test-file=src/sum.test.js
== CHECKS == ✔ 1 passed
✔ dag://?check=test&jest-project=web&jest-test-file=src/cart.test.js&jest-test-file=src/sum.test.js 1.1s OK
== TESTS ==
✔ src/cart.test.js 2 passed
✔ src/sum.test.js 1 passed
✔ 3 passed
When you select only some files, the module runs jest --passWithNoTests --runTestsByPath with those files. To list the keys that you can select, run dagger list jest-projects -a or dagger list jest-test-files -a.
The module finds test files without a run of Jest. It reads testMatch, testRegex, testPathIgnorePatterns, roots, and rootDir when they are literal values. For other values, it uses Jest’s defaults. It skips node_modules and the files that .gitignore excludes. To see the files that Jest itself finds, call the project’s list function:
dagger -c 'jest | projects | get web | list'
/src/web/src/cart.test.js
/src/web/src/receipt.test.js
/src/web/src/sum.test.js
The directory that you run dagger from also selects projects. Dagger selects the projects at or below it, and the nearest project that contains it.
When a test fails, the report gives a DAG link. This address selects the same files again, in the same container, on any machine:
✘ 2 failed
✔ 1 passed
== RUN LOCALLY ==
dagger check "dag://?check=test&jest-project=web&jest-test-file=src/cart.test.js&jest-test-file=src/sum.test.js"
Split a large suite across machines
The module runs the selected files of a project in one Jest process. For a large suite, Dagger splits the tests across engines instead. It balances the parts with the measured resource use, input cost, and cache state of each test (test splitting). You do not fix a shard count or write a custom sequencer.
Do not put --shard in the flags setting. The module passes flags to every jest run in the workspace, so every check runs only one shard. In a project with three test files and flags set to ["--shard=1/2"], the check ran two of the files:
dagger module settings jest flags '["--shard=1/2"]'
dagger check
== CHECKS == ✔ 1 passed
✔ dag://?check=test&jest-project=web 2.5s OK
== TESTS ==
✔ src/receipt.test.js 1 passed
✔ src/sum.test.js 1 passed
✔ 2 passed
To remove the setting, run dagger module settings -u jest flags.
Run the tests locally and with Cloud Checks
Cloud Checks run your checks on Dagger’s Cloud Engines for each push and pull request, with no CI workflow file. Cloud Engines are Dagger Engines that Dagger Cloud runs for you. The repository must be on GitHub, and its Git origin remote must point to it. Turn on Cloud Checks from the project root:
dagger cloud checks on
If you do not have a Dagger Cloud account, the command tells you to run dagger cloud signup first. Commit dagger.toml and dagger.lock, then push. Each push then runs the same check, with the same Node image and the same CI mode.
The results show as GitHub check statuses and in Dagger Cloud (changelog). The module reports each test as its own result, so Dagger Cloud shows each test. To see recent runs from the terminal, run dagger workspace activity. If a check fails, run dagger check locally to see the same failure.
Bottom line: Run the project’s own Jest on a supported Node version with --ci, so new snapshots fail. Split a large suite with --shard, and write a custom testSequencer for a split by anything other than file count. Dagger’s Jest module meets the same requirements in one place. It pins the Node image in dagger.toml, runs in CI mode, and selects single files with --jest-test-file. It runs the same check locally and with Cloud Checks.
Related guides
- How do I run Vitest in CI, with sharding and merged coverage?: the same requirements for a project that tests with Vitest.
- How do I run Playwright tests in CI and locally in the same container, with cached browsers?: browser tests next to Jest unit tests.
- How do I run ESLint the same way locally and in CI?: lint the same project with its own ESLint.