Pass Cypress Test Info Via Request Headers

Attach Cypress test run information to the API calls using `cy.intercept` middleware.

Imagine the application under test is making web calls to the backend. Sometimes things go wrong, and you have set up server-side logging to debug the failures. It would be nice having the test run information attached to each API call the application makes while Cypress is running its tests. In this blog post I will show how to set such information via X-... request headers. We can easily send the following information with each app call:

  • the current test title
  • parts of the test title, like test case IDs
  • test tags from @bahmutov/cy-grep plugin
  • CI server and job information

The server can log the X-... request headers to let you find the relevant information faster.

Example X headers received by the server

🎁 You can find the full source code for this blog post in the repo bahmutov/cypress-api-headers-example.

Intercept middleware

We can spy and stub application network calls using the cy.request command. I even have an entire course of network testing exercises, but how do you spy on every network call? By using the "middleware" option. For example, let's add the current test title to the outgoing request:

cypress/support/e2e.js
1
2
3
4
5
6
7
8
beforeEach(() => {
const currentTestTitle = Cypress.currentTest.title

cy.intercept({ resourceType: /fetch|xhr/ }, (req) => {
req.headers['X-Test-Source'] = 'Cypress'
req.headers['X-Test-Title'] = currentTestTitle
})
})

The server should see the two lowercase X-... headers on every Ajax call sent by the application. The Command Log shows the modified outgoing request:

The Cypress Command Log shows the request

TestRail case ID

If you are using TestRail to manage your test cases, you might be using my plugin cypress-testrail-simple. I like putting the test case ID into the test title:

cypress/e2e/spec.cy.js
1
2
3
4
5
6
7
describe('API', () => {
it('C123456 serves the homepage', () => {
cy.visit('/home.html')
cy.contains('h1', 'Homepage')
cy.contains('button', 'Click me').click()
})
})

We can parse the test case and send it in a separate request header:

cypress/support/e2e.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
beforeEach(() => {
const currentTestTitle = Cypress.currentTest.title
// there might be TestRail case id in the title
// try extracting it separately
const testRailId =
currentTestTitle.match(/(?<caseId>C\d+)/)?.groups?.caseId

cy.intercept({ resourceType: /fetch|xhr/ }, (req) => {
req.headers['X-Test-Source'] = 'Cypress'
req.headers['X-Test-Title'] = currentTestTitle
if (testRailId) {
req.headers['X-Test-Rail-Id'] = testRailId
}
})
})

Effective Test Tags

If you are using my plugin @bahmutov/cy-grep to filter the tests to run the How To Tag And Run End-to-End Tests, you can send the effective test tags in the request headers. Here is an example spec showing several levels of test tags:

cypress/e2e/test-tags.cy.js
1
2
3
4
5
6
describe('API', { tags: '@static' }, () => {
it('Visible button', { tags: '@sanity' }, () => {
cy.visit('/home.html')
cy.contains('button', 'Click me').click()
})
})

The test "Visible button" has 2 effective test tags: @sanity comes from the test itself. The test tag @static comes from its parent suite "API". The support E2E file can grab the effective test tags

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import registerCypressGrep from '@bahmutov/cy-grep/src/support'
registerCypressGrep()

beforeEach(() => {
const currentTestTitle = Cypress.currentTest.title
// there might be TestRail case id in the title
// try extracting it separately
const testRailId =
currentTestTitle.match(/(?<caseId>C\d+)/)?.groups?.caseId

// the effective test tags are set by the plugin
// @bahmutov/cy-grep
const testTags = Cypress.env('testTags')

cy.intercept({ resourceType: /fetch|xhr/ }, (req) => {
req.headers['X-Test-Source'] = 'Cypress'
req.headers['X-Test-Title'] = currentTestTitle
if (testRailId) {
req.headers['X-Test-Rail-Id'] = testRailId
}

if (testTags && testTags.length) {
req.headers['X-Test-Tags'] = testTags.join(',')
}
})
})

CI information

I like running my E2E tests using GitHub Actions. Let's pass CI runtime information too. For simplicity, I will pass the CI runner numbers via CYPRESS_ environment variables.

.github/workflows/ci.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
name: ci
on: push
jobs:
tests:
runs-on: ubuntu-22.04
steps:
- name: Print GitHub CI variables
run: npx @bahmutov/print-env GITHUB
- name: Checkout
uses: actions/checkout@v4
- name: Cypress run
uses: cypress-io/github-action@v6
with:
start: npm start
# pass CI run information via Cypress_ variables
# to make it readily available in Cypress specs and support files
env:
CYPRESS_runId: ${{ github.run_id }}
CYPRESS_runNumber: ${{ github.run_number }}
CYPRESS_runAttempt: ${{ github.run_attempt }}
CYPRESS_jobName: ${{ github.job }}

The support file can grab each CI variable using the Cypress.env command

cypress/support/e2e.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import registerCypressGrep from '@bahmutov/cy-grep/src/support'
registerCypressGrep()

beforeEach(() => {
const currentTestTitle = Cypress.currentTest.title
// there might be TestRail case id in the title
// try extracting it separately
const testRailId =
currentTestTitle.match(/(?<caseId>C\d+)/)?.groups?.caseId

// the effective test tags are set by the plugin
// @bahmutov/cy-grep
const testTags = Cypress.env('testTags')

cy.intercept({ resourceType: /fetch|xhr/ }, (req) => {
req.headers['X-Test-Source'] = 'Cypress'
req.headers['X-Test-Title'] = currentTestTitle
if (testRailId) {
req.headers['X-Test-Rail-Id'] = testRailId
}
if (Cypress.env('runId')) {
req.headers['X-Run-Id'] = Cypress.env('runId')
}
if (Cypress.env('runNumber')) {
req.headers['X-Run-Number'] = Cypress.env('runNumber')
}
if (Cypress.env('runAttempt')) {
req.headers['X-Run-Attempt'] = Cypress.env('runAttempt')
}
if (Cypress.env('jobName')) {
req.headers['X-Job-Name'] = Cypress.env('jobName')
}

if (testTags && testTags.length) {
req.headers['X-Test-Tags'] = testTags.join(',')
}
})
})

Here is what the server sees on a typical test run:

Custom X- headers for a test with TestRail test case id

Custom X- headers for a test with test tags

Nice.