As your project grows, the end-to-end tests take longer and longer to finish. You open a pull request and ... wait for 10-20 minutes for the tests to finish. Then you search the Cypress Dashboard to find the spec with the modified test, just to see if it has failed or passed. All this time, you are thinking to yourself - why can't Cypress run the modified specs first? While there is no built-in way in Cypress as of October 2021, it is not hard to implement it yourself. In this blog post, I will show how to run new and changed Cypress.io specs first if you are using GitHub Actions. Similar approach could be used with any CI provider, like CircleCI.
- The initial workflow
- The pull request workflow
- Limit the Git output
- Workflow in action
- See also
- Update 1: pick the changed specs better
- Update 2: pick the changed specs better part 2
- See also
๐ You can find the source code for this blog post in the repo bahmutov/chat.io
The initial workflow
At first, our GitHub workflow file checks out the source code and runs tests using Cypress GH Action I have written:
1 |
|
The above workflow runs on every commit and on every pull request. We still want to run all the tests for every commit pushed to the main
branch. But for the pull requests, we want to run the modified specs first before running all tests. Thus I modify the above ci
workflow to only run on commits pushed to the main branch.
1 | name: ci |
You can find this workflow in .github/workflows/ci.yml. This workflow is tied to the README badge, showing the current test status of the project:
1 | [![ci status][ci image]][ci url] |
The pull request workflow
I will use a separate workflow file for CI steps to run for the pull requests. You can find the finished workflow file at .github/workflows/pr.yml. Let's start by cloning the ci.yml
and just modifying the on
trigger.
1 | name: pr |
First, we will need to check out the source code. Because we want to determine the files changed between the PR branch and the default main
branch, we need to fetch this information. Thus I will use the parameter fetch-depth: 0
with actions/checkout action:
1 | # https://github.com/actions/checkout |
Here is how we can find all changed (added and modified) files between the current branch and the main
branch
1 | - name: List changed files ๐ |
For example, I have started a new branch example-branch
and modified the spec rooms.js
and added a new spec ``
Let's commit and push this branch to the remote origin.
1 | git add . |
Even when working locally, we can see the changed files between the current branch example-branch
and the main
one.
1 | $ git diff --name-only origin/main |
The pull request might have other modified files besides the Cypress specs. For example, I will touch the README file too. Here is how we can filter the specs
1 | git diff --name-only origin/main |
Super. Later we will need to know the number of modified specs - we can use wc -l
to count the lines with the modified Cypress specs
1 | git diff --name-only origin/main | grep cypress/integration | wc -l |
Ughh, why is there whitespace around 2
, let's trim it
1 | git diff --name-only origin/main | grep cypress/integration | wc -l | tr -d ' ' |
Now that we know the number of changed specs, let's also join them into a single string to be passed to the cypress run --spec ...
parameter.
1 | git diff --name-only origin/main | grep cypress/integration | tr '\n' ',' |
Super. We can compute the number and the spec parameter in the workflow, and even hide the details from other specs by using the output
parameters.
1 | - name: List changed files ๐ |
Other workflow steps can access the number of changed specs using ${{ steps.list-changed-specs.outputs.specsN }}
expression syntax. Let's set up two test jobs - the first one will run if there are changed specs, but not more than 5. If there are lots of modified specs, it makes sense to simply run all of them.
1 | # https://github.com/cypress-io/github-action |
We have two problems in the above workflow:
- we install the NPM dependencies twice (potentially)
- we are trying to run the application using
npm start
twice (potentially)
Thus we can optimize the workflow by installing the dependencies just once, and starting the application before running any Cypress tests.
1 | # install dependencies |
Tip: Cypress GH Action can do a lot, find all examples in the cypress-io/github-action repo.
Limit the Git output
Two words of caution: the command git diff --name-only origin/main
outputs all file names, including the names of the deleted files. Thus I limit the list of the modified and added files only using
1 | git diff --name-only --diff-filter=AM origin/main |
You should also be careful about printing the list of changed files. By itself the above command will page the output and pause after N lines. This will halt the CI job usually:
1 | $ git diff --name-only --diff-filter=AM origin/main |
Thus you want to pipe the output through the filters first - the filters do not get paginated. For example, you can get the raw number of changed files.
1 | n=$(git diff --name-only --diff-filter=AM origin/main | grep cypress/integration | wc -l | tr -d ' ') |
Workflow in action
Let's see how the above workflow performs. I have opened the pull request #12.
The GH workflow shows the steps, and that the modified specs task was executed
The list-changed-spec
step has calculated the two changed Cypress test files correctly
The changed tests have finished successfully, while all tests have failed in an unrelated spec group-chat.js
. Notice how fast the modified specs have finished vs waiting for all the tests: 30 seconds vs 5 minutes.
The group-chat.js
shows the test fails to log in the first user A
Let's modify the group-chat.js
- something goes wrong there, let's change the user name to be a userA
instead of just A
. Once I push the commit with the username change, the 3 changes specs run:
Our fix has solved the problem, and all the Cypress specs have passed. We are good to merge.
See also
- Get Faster Feedback From Your Cypress Tests Running On CircleCI
- Trying GitHub Actions
- Split Long GitHub Action Workflow Into Parallel Cypress Jobs
Update 1: pick the changed specs better
What happens if you modify a non-spec file in the cypress/integration
folder? For example, if you touch the cypress/integration/utils.js
that is excluded by the cypress.json
file, our crude "give me the changed files" filter tries to run this file by itself, causing the Cypress to exit with an error code "No specs found".
This is where we can use my utility find-cypress-specs to find the changed spec files. You can ask this utility for only the specs changed against a Git branch.
1 | specs=$(npx find-cypress-specs --branch main) |
This number and the list are computed using the Git and the cypress.json file settings.
Update 2: pick the changed specs better part 2
When using find-cypress-specs in a busy repository, I found a shortcoming: the added and changed specs against the main branch picked up specs changed on the main branch in addition to the specs changed in the pull request branch. So I added another option to the CLI tool: pick the changed specs only between the current commit and its parent merge commit. The best way to show the difference is via a diagram below.
Let's say we want to find the changed specs between the last commit on the branch "FeatureA" and the current head of branch "main". It would give us the changed files between the two branches.
1 | # from the branch "FeatureA" |
We are probably not interested in the specs spec3.js
and spec4.js
added on the "main" branch - they are not part of the feature work. To limit the changed specs to search the current branch up to the merge commit "C1" we can use the --parent
flag.
1 | # from the branch "FeatureA" |
I think it makes a lot more sense, doesn't it?
Tip: if you use cypress-grep to tag and selectively run tests, you can filter the changed specs by the presence of tag(s). For example, to run only the changed specs that have the tag "@user" inside, use npx find-cypress-specs --branch main --parent --tagged @user
.