Gathering detailed insights and metrics for cypress-split
Gathering detailed insights and metrics for cypress-split
Gathering detailed insights and metrics for cypress-split
Gathering detailed insights and metrics for cypress-split
cypress-circleci-reporter
Cypress test reporter for CircleCI
cypress-on-fix
Fixes multiple Cypress plugins subscribing to "on" events
cypress-parallel
Reduce up to 40% your Cypress suite execution time parallelizing the test run on the same machine.
cypress
Cypress is a next generation front end testing tool built for the modern web
Split Cypress specs across parallel CI machines for speed
npm install cypress-split
Module System
Min. Node Version
Typescript Support
Node Version
NPM Version
221 Stars
321 Commits
25 Forks
4 Watching
39 Branches
10 Contributors
Updated on 27 Nov 2024
JavaScript (99.32%)
HTML (0.68%)
Cumulative downloads
Total Downloads
Last day
-12.6%
25,298
Compared to previous day
Last week
3.8%
150,905
Compared to previous week
Last month
17.3%
601,057
Compared to previous month
Last year
420.7%
4,907,383
Compared to previous year
Split Cypress specs across parallel CI machines for speed without using any external services
Add this plugin as a dev dependency and include in your Cypress config file.
# install using NPM
$ npm i -D cypress-split
# install using Yarn
$ yarn add -D cypress-split
Call this plugin from your Cypress config object setupNodeEvents
method:
1// cypress.config.js 2const { defineConfig } = require('cypress') 3// https://github.com/bahmutov/cypress-split 4const cypressSplit = require('cypress-split') 5 6module.exports = defineConfig({ 7 e2e: { 8 setupNodeEvents(on, config) { 9 cypressSplit(on, config) 10 // IMPORTANT: return the config object 11 return config 12 }, 13 }, 14})
⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
Important: return the config
object from the setupNodeEvents
function. Otherwise, Cypress will run all specs.
⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
1// cypress/plugins/index.js 2// https://github.com/bahmutov/cypress-split 3const cypressSplit = require('cypress-split') 4 5module.exports = (on, config) => { 6 cypressSplit(on, config) 7 // IMPORTANT: return the config object 8 return config 9}
Now update your CI script:
Run several containers and start Cypress using --env split=true
parameter
1stages: 2 - build 3 - test 4 5install: 6 image: cypress/base:16.14.2-slim 7 stage: build 8 script: 9 - npm ci 10 11test: 12 image: cypress/base:16.14.2-slim 13 stage: test 14 parallel: 3 15 script: 16 - npx cypress run --env split=true
All specs will be split into 3 groups automatically. For caching details, see the full example in gitlab.com/bahmutov/cypress-split-gitlab-example.
1parallelism: 3 2command: npx cypress run --env split=true
See the full example in bahmutov/cypress-split-example
1npx cypress run --env split=true
The script uses BUILDKITE_PARALLEL_JOB_COUNT
and BUILDKITE_PARALLEL_JOB
environment variables.
1# run 3 copies of the current job in parallel 2strategy: 3 fail-fast: false 4 matrix: 5 containers: [1, 2, 3] 6steps: 7 - name: Run split Cypress tests 🧪 8 uses: cypress-io/github-action@v6 9 # pass the machine index and the total number 10 env: 11 SPLIT: ${{ strategy.job-total }} 12 SPLIT_INDEX: ${{ strategy.job-index }}
Note that we need to pass the SPLIT
and SPLIT_INDEX
numbers from the strategy
context to the plugin to grab. See the full example in bahmutov/cypress-split-example
Sample Jenkins File to run scripts on parallel:
1pipeline { 2 agent { 3 // this image provides everything needed to run Cypress 4 docker { 5 image 'cypress/base' 6 } 7 } 8 9 stages { 10 // first stage installs node dependencies and Cypress binary 11 stage('build') { 12 steps { 13 // there a few default environment variables on Jenkins 14 // on local Jenkins machine (assuming port 8080) see 15 // http://localhost:8080/pipeline-syntax/globals#env 16 echo "Running build ${env.BUILD_ID} on ${env.JENKINS_URL}" 17 sh 'npm ci' 18 sh 'npm run cy:verify' 19 } 20 } 21 22 stage('start local server') { 23 steps { 24 // start local server in the background 25 // we will shut it down in "post" command block 26 sh 'nohup npm run start &' 27 } 28 } 29 30 // this stage runs end-to-end tests, and each agent uses the workspace 31 // from the previous stage 32 stage('cypress parallel tests') { 33 environment { 34 // Because parallel steps share the workspace they might race to delete 35 // screenshots and videos folders. Tell Cypress not to delete these folders 36 CYPRESS_trashAssetsBeforeRuns = 'false' 37 } 38 39 // https://jenkins.io/doc/book/pipeline/syntax/#parallel 40 parallel { 41 // start several test jobs in parallel, and they all 42 // will use Cypress Split to load balance any found spec files 43 stage('set A') { 44 steps { 45 echo "Running build ${env.BUILD_ID}" 46 sh "npx cypress run --env split=2,splitIndex=0" 47 } 48 } 49 50 // second thread runs the same command 51 stage('set B') { 52 steps { 53 echo "Running build ${env.BUILD_ID}" 54 sh "npx cypress run --env split=2,splitIndex=1" 55 } 56 } 57 } 58 } 59 } 60 61 post { 62 // shutdown the server running in the background 63 always { 64 echo 'Stopping local server' 65 sh 'pkill -f http-server' 66 } 67 } 68}
If you are running N containers in parallel, pass the zero-based index and the total number to the plugin using the environment variables SPLIT_INDEX
and SPLIT
or via Cypress env option:
# using process OS environment variables
job1: SPLIT=3 SPLIT_INDEX=0 npx cypress run
job2: SPLIT=3 SPLIT_INDEX=1 npx cypress run
job3: SPLIT=3 SPLIT_INDEX=2 npx cypress run
# using Cypress env option
job1: npx cypress run --env split=3,splitIndex=0
job2: npx cypress run --env split=3,splitIndex=1
job3: npx cypress run --env split=3,splitIndex=2
Some CIs provide an agent index that already starts at 1. You can pass it via SPLIT_INDEX1
instead of SPLIT_INDEX
job1: SPLIT=3 SPLIT_INDEX1=1 npx cypress run
If you know the spec timings, you can create a JSON file and pass the timings to this plugin. The list of specs will be split into N machines to make the total durations for each machine approximately equal. You can see an example timings.json file:
1{ 2 "durations": [ 3 { 4 "spec": "cypress/e2e/chunks.cy.js", 5 "duration": 300 6 }, 7 { 8 "spec": "cypress/e2e/spec-a.cy.js", 9 "duration": 10050 10 }, 11 ... 12 ] 13}
You can pass the JSON filename via SPLIT_FILE
environment variable or Cypressenv
variable.
# split all specs across 3 machines using known spec timings
# loaded from "timings.json" file
$ SPLIT_FILE=timings.json SPLIT=3 npx cypress run
# the equivalent syntax using Cypress --env argument
$ npx cypress run --env split=3,splitFile=timings.json
For specs not in the timings file, it will use average duration of the known specs. The timings file might not exist, in this case the specs are split by name. At the end of the run, the duration of all run specs is printed and can be saved into the timings JSON file. Note: you would need to combine the timings from different runners into a single JSON file yourself.
If the timings file does not exist yet, the timings will be written into the file after the run finishes. If the file exists, and the new timings have new entries or the existing entries are off by more than 10% duration, the merged file is written back. Timing for specs without any passes tests or with failed tests is ignored. You can control the threshold to avoid changing the timings file if the times are too close. For example, to only update the timings file if any duration is different by 20% you can use the environment variable SPLIT_TIME_THRESHOLD
$ SPLIT_TIME_THRESHOLD=0.2 SPLIT_FILE=... npx cypress run ...
See example bahmutov/cypress-split-timings-example.
Note 2: during Cypress execution, the working directory is set to the folder with the Cypress config file. This module tries its best to find the split file by searching the parent folders to the Git repo or root folder.
Typically you want to find all specs and split them into chunks. But you can adjust the final list of specs using your own callback function.
1// the specs is the list of specs
2// determined by the split algorithm
3function adjustTheSpecs(specs) {
4 // for example, reverse the order of specs
5 specs.reveres()
6 return specs
7}
8
9module.exports = defineConfig({
10 e2e: {
11 setupNodeEvents(on, config) {
12 cypressSplit(on, config, adjustTheSpecs)
13 // IMPORTANT: return the config object
14 return config
15 },
16 },
17})
Using your own callback function you can insert new specs to run at the beginning or run the specs in certain custom order, etc. Make sure the returned value is an array of absolute spec paths.
You can see how this plugin is going to split the specs using the cypress-split-preview
alias
# show the split across N machines
$ npx cypress-split-preview --split <N>
# show the split across N machines based on spec timings
$ npx cypress-split-preview --split <N> --split-file <JSON filename>
This module includes a bin utility to merge multiple timings files into one. Example:
npx cypress-split-merge \
--parent-folder partials/ \
--split-file timings.json \
--output out-timings.json \
--set-gha-output merged-timing
The above command finds all timings.json
file in the sub folders of partials/
folder and merges them. It saved the result to out-timings.json
file and if running on GitHub Actions sets the job output named merged-timing
to a stringified single line JSON line.
You can also indicate where the plugin should output the timings, by setting the SPLIT_OUTPUT_FILE
environment variable or the corresponding Cypress env
variable. This will specify the file where the timings will be written. If SPLIT_OUTPUT_FILE
is not set, the plugin will default to using the same file specified in SPLIT_FILE
.
# Set the SPLIT_OUTPUT_FILE environment variable
$ SPLIT_FILE=timings.json SPLIT_OUTPUT_FILE=output.json npx cypress run
# Or use the Cypress --env option
$ npx cypress run --env splitFile=timings.json,splitOutputFile=output.json
To skip GitHub Actions summary, set an environment variable SPLIT_SUMMARY=false
. By default, this plugin generates the summary.
Works the same way as splitting E2E specs. Add this plugin to the setupNodeEvents
callback in the component
object in the config. See cypress.config.js for example:
1// cypress.config.js
2const { defineConfig } = require('cypress')
3const cypressSplit = require('cypress-split')
4
5module.exports = defineConfig({
6 e2e: {
7 // baseUrl, etc
8 },
9
10 component: {
11 devServer: {
12 framework: 'react',
13 bundler: 'vite',
14 },
15 specPattern: 'components/*.cy.js',
16 setupNodeEvents(on, config) {
17 cypressSplit(on, config)
18 // IMPORTANT: return the config object
19 return config
20 },
21 },
22})
Described in the blog post Split React Native Web Component Tests For Free.
This plugin finds the Cypress specs using find-cypress-specs and then splits the list into chunks using the machine index and the total number of machines. On some CIs (GitLab, Circle), the machine index and the total number of machines are available in the environment variables. On other CIs, you have to be explicit and pass these numbers yourself.
1// it works something like this:
2setupNodeEvents(on, config) {
3 const allSpecs = findCypressSpecs()
4 // allSpecs is a list of specs
5 const chunk = getChunk(allSpecs, k, n)
6 // chunk is a subset of specs for this machine "k" of "n"
7 // set the list as the spec pattern
8 // for Cypress to run
9 config.specPattern = chunk
10 return config
11}
Suppose you want to run some specs first, for example just the changed specs. You would compute the list of specs and then call Cypress run
command with the --spec
parameter
$ npx cypress run --spec "spec1,spec2,spec3"
You can still split the specs across several machines using cypress-split
, just move the --spec
list (or duplicate it) to a process or Cypress env variable spec
:
# using process environment variables split all specs across 2 machines
$ SPEC="spec1,spec2,spec3" SPLIT=2 SPLIT_INDEX=0 npx cypress run --spec "spec1,spec2,spec3"
$ SPEC="spec1,spec2,spec3" SPLIT=2 SPLIT_INDEX=1 npx cypress run --spec "spec1,spec2,spec3"
# using Cypress "env" option
$ npx cypress run --env split=2,splitIndex=0,spec="spec1,spec2,spec3"
$ npx cypress run --env split=2,splitIndex=1,spec="spec1,spec2,spec3"
# for CIs with automatically index detection
$ npx cypress run --env split=true,spec="spec1,spec2,spec3"
Inside the SPEC=....
value you can use wildcards, for example to run all specs inside a subfolder
$ SPEC="cypress/e2e/featureA/*.cy.js" npx cypress run --spec "cypress/e2e/featureA/*.cy.js"
Important: if you are passing the list of specs using --env spec="..."
and get the error Cannot parse as valid JSON
, switch to using SPEC
environment variable, see #79.
# instead of
$ npx cypress run --env spec="..." --spec "..."
Cannot parse as valid JSON
# use
$ SPEC=... npx cypress run --spec "..."
You can pass a list of specs to exclude before splitting up across the machines.
SKIP_SPEC="spec1,spec2" SPLIT=2 ...
# finds the list of specs and removes "spec1" and "spec2"
# before dividing across two machines
If your spec
pattern includes wildcards *
then they will be resolved using globby
module.
# split all specs inside the `cypress/e2e` folder
SPEC="cypress/e2e/**/*.cy.js" npx cypress run --spec "cypress/e2e/**/*.cy.js"
# or the equivalent using --env parameter
npx cypress run --spec "cypress/e2e/**/*.cy.js" --env spec="cypress/e2e/**/*.cy.js"
You can shuffle the found specs before splitting using a stable seed
$ SPLIT_RANDOM_SEED=42 npx cypress run ...
This is useful to randomize the order of specs to find any dependencies between the tests.
Note: all parallel machines usually compute the list of specs, thus the seed must be the same to guarantee the same list is generated and split correctly, otherwise some specs would be "lost".
If cypress-split
has SPLIT
and the index and finds the specs, it sets the list of specs in the config
object
1setupNodeEvents(on, config) {
2 cypressSplit(on, config)
3 // config.specPattern is a string[]
4 // of absolute filenames
5 return config
6}
Some situations require relative spec names, for example in Angular component specs. You can transform the specs into relative form yourself before returning the config:
1setupNodeEvents(on, config) { 2 cypressSplit(on, config) 3 if (Array.isArray(config.specPattern)) { 4 // change the absolute filenames to relative 5 config.specPattern = config.specPattern.map((file) => { 6 return file.replace(process.cwd(), '.') 7 }) 8 } 9 return config 10}
Should work just the same, see the tested example in bahmutov/cypress-split-cucumber-example
1// cypress.config.js 2const { defineConfig } = require('cypress') 3// https://github.com/bahmutov/cypress-split 4const cypressSplit = require('cypress-split') 5const cucumber = require('cypress-cucumber-preprocessor').default 6 7module.exports = defineConfig({ 8 e2e: { 9 setupNodeEvents(on, config) { 10 cypressSplit(on, config) 11 12 on('file:preprocessor', cucumber()) 13 // IMPORTANT: return the config object 14 return config 15 }, 16 specPattern: 'cypress/e2e/**/*.feature', 17 }, 18})
To see diagnostic log messages from this plugin, set the environment variable DEBUG=cypress-split
. It will give you all information necessary to understand what specs the plugin finds and how it splits them up. Here is an example debug output:
Tip: cypress-split
uses find-cypress-specs to discover specs. If something is wrong, it is useful to see debug messages from both modules:
DEBUG=cypress-split,find-cypress-specs npx cypress run
for example, if using GitHub Actions:
1- name: Run split Cypress tests 🧪 2 uses: cypress-io/github-action@v6 3 # pass the machine index and the total number 4 env: 5 SPLIT: ${{ strategy.job-total }} 6 SPLIT_INDEX: ${{ strategy.job-index }} 7 DEBUG: 'cypress-split,find-cypress-specs'
If you notice that the plugin is not working as expected, and you are registering multiple Cypress plugins, you might be experiencing Cypress issue #22428. Use cypress-on-fix to register the plugins:
1const { defineConfig } = require('cypress') 2 3module.exports = defineConfig({ 4 e2e: { 5 // baseUrl, etc 6 supportFile: false, 7 fixturesFolder: false, 8 setupNodeEvents(cypressOn, config) { 9 const on = require('cypress-on-fix')(cypressOn) 10 // use "on" to register plugins, for example 11 // https://github.com/bahmutov/cypress-split 12 require('cypress-split')(on, config) 13 // https://github.com/bahmutov/cypress-watch-and-reload 14 require('cypress-watch-and-reload/plugins')(on, config) 15 // https://github.com/bahmutov/cypress-code-coverage 16 require('@bahmutov/cypress-code-coverage/plugin')(on, config) 17 18 // IMPORTANT: return the config object 19 return config 20 }, 21 }, 22})
Types are in src/types.d.ts file. You should be able to import the config function in your TS config file.
1import { defineConfig } from 'cypress'
2import cypressSplit from 'cypress-split'
3
4module.exports = defineConfig({
5 e2e: {
6 // baseUrl, etc
7 supportFile: false,
8 fixturesFolder: false,
9 setupNodeEvents(on, config) {
10 cypressSplit(on, config)
11 // IMPORTANT: return the config object
12 return config
13 },
14 },
15})
If you are using many Cypress plugins (for example my plugins covered in the Cypress Plugins course), you might notice that only the last plugin really works. This is due to a bug, and you can work around it using cypress-on-fix.
If you set your own specs via config.specPattern
, just do it before using the plugin to split them.
1setupNodeEvents(on, config) { 2 // user sets their own custom specPattern list of specs 3 // make sure the list of specs is relative to the folder 4 // with the Cypress config file! 5 config.specPattern = [ 6 '../cypress/e2e/spec-c.cy.js', 7 '../cypress/e2e/spec-d.cy.js', 8 '../cypress/e2e/spec-e.cy.js', 9 ] 10 cypressSplit(on, config) 11 // IMPORTANT: return the config object 12 return config 13},
Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2023
License: MIT - do anything with the code, but don't blame me if it does not work.
Support: if you find a problem, open an issue in this repository. Consider sponsoring my open-source work.
No vulnerabilities found.
No security vulnerabilities found.