Writing Tests That Depend On Other Tests

If you must use a Cypress anti-pattern, at least do it right using cypress-data-session plugin.

Cypress Best Practices strongly advocates for keeping the tests independent of each other.

Tests should be independent of each other

I strongly agree with this advice. We should be able to run each test by itself and run the tests in any order. But sometimes it is simpler to have one test reuse state left by the previous test. In this blog post, I will show I solve this problem in somewhat ok way by using my plugins.

The dependent tests

Let's start with a spec file with two tests.

cypress/e2e/spec1.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let projectName

it('creates item A', () => {
cy.log('a very long test')
cy.wait(1000)
cy.log('that creates project').then(() => {
// how do we pass the data created in this test
// to the next test?
projectName = `my random project ${Cypress._.random(1e4)}`
cy.log(projectName)
})
})

// this test MUST run only after the previous test runs
// you cannot run it by itself using "it.only"
it('continues working with data created in the previous test', () => {
cy.wrap(projectName, { timeout: 0 }).should('include', 'project')
})

🎁 You can find the source code for this blog post in the repo bahmutov/cypress-dependent-test-example.

If the tests run together, everything looks good. The second test gets the random piece of data from the first test.

The first solution using a local variable

What if we want to run the second test by itself? Even after running the tests together, if we change it to it.only the spec reloads and fails - because the entire spec is evaluated again, and the variable definition let projectName is undefined by default.

cypress/e2e/spec1.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let projectName

it('creates item A', () => {
cy.log('a very long test')
cy.wait(1000)
cy.log('that creates project').then(() => {
// how do we pass the data created in this test
// to the next test?
projectName = `my random project ${Cypress._.random(1e4)}`
cy.log(projectName)
})
})

// this test MUST run only after the previous test runs
// you cannot run it by itself using "it.only"
it.only('continues working with data created in the previous test', () => {
cy.wrap(projectName, { timeout: 0 }).should('include', 'project')
})

Cannot run the second test by itself

How can we "preserve" the value projectName?

Storing value in Cypress.env object

We can try using Cypress.env object to store the projectName value. It does not solve the problem - Cypress v10 resets the Cypress.env() object back to the initial project settings before each spec.

cypress/e2e/spec1-env.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
it('creates item A', () => {
cy.log('a very long test')
cy.wait(1000)
cy.log('that creates project').then(() => {
const projectName = `my random project ${Cypress._.random(1e4)}`
Cypress.env('projectName', projectName)
cy.log(projectName)
})
})

// this test MUST run only after the previous test runs
// you cannot run it by itself using "it.only"
it.only('continues working with data created in the previous test', () => {
const projectName = Cypress.env('projectName')
cy.wrap(projectName, { timeout: 0 }).should('include', 'project')
})

Values stored in Cypress.env are wiped before the test run

We need more permanent storage.

Using cypress-plugin-config

To preserve the values across test re-runs I wrote plugin cypress-plugin-config. It stores the values using window.top.cypressPluginConfig object. Cypress resets the iframes inside of the browser window (one is the spec iframe, another one is the application iframe), but the top window stays.

cypress/e2e/spec2.cy.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
import {
getPluginConfigValue,
setPluginConfigValue,
} from 'cypress-plugin-config'

it('creates item A', () => {
cy.log('a very long test')
cy.wait(1000)
cy.log('that creates project').then(() => {
// how do we pass the data created in this test
// to the next test?
const projectName = `my random project ${Cypress._.random(1e4)}`
cy.log(projectName)
setPluginConfigValue('project name', projectName)
})
})

it('continues working with data created in the previous test', () => {
const projectName = getPluginConfigValue('project name')
expect(projectName, 'got project name').to.be.a(
'string',
'project name created by previous test',
)
cy.wrap(projectName, { timeout: 0 }).should('include', 'project')
})

Let's say the spec runs both tests. It passes and creates the "project 7373" data item.

The first project stores the generated data using cypress-plugin-config

Great, now let's run the second test by itself using it.only

1
2
3
4
5
6
7
8
it.only('continues working with data created in the previous test', () => {
const projectName = getPluginConfigValue('project name')
expect(projectName, 'got project name').to.be.a(
'string',
'project name created by previous test',
)
cy.wrap(projectName, { timeout: 0 }).should('include', 'project')
})

The second test works by itself

Great - the second test still runs even after the spec reloads. The piece of data generated by the first test is still there, retrieved by the function getPluginConfigValue.

Unfortunately, that is not the end of the story. While the data is there when the spec file changes on disk, or if the user clicks the "Rerun tests" button, if we do the hard browser reload using Cmd-R keys, the top window is reloaded, and our data is wiped away too.

The data in the top window is wiped when we do hard reload

We need an even more permanent data storage that survives the hard browser reload (or even the browser relaunch)

Store data using cypress-data-session

Cypress tests run in the browser. But the initial process that starts the browser runs in Node and the specs can communicate with that process using the powerful cy.task command. The Node process stays running and can store any data for us, see the video Pass Data From One Spec To Another for example. I have implemented storing data on demand in my universal caching plugin cypress-data-session. Here is how we can utilize it to solve our current problem when one test needs to pass some data to another test:

cypress.config.js
1
2
3
4
5
6
7
8
9
10
const { defineConfig } = require('cypress')

module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
// https://github.com/bahmutov/cypress-data-session
require('cypress-data-session/src/plugin')(on, config)
},
},
})
cypress/e2e/spec3.cy.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
import 'cypress-data-session'

it('creates item A', () => {
cy.log('a very long test')
cy.wait(1000)
cy.log('that creates project').then(() => {
// how do we pass the data created in this test
// to the next test?
const projectName = `my random project ${Cypress._.random(1e4)}`
cy.log(projectName)
cy.dataSession({
name: 'project name',
setup() {
return projectName
},
shareAcrossSpecs: true,
})
})
})

// this test after the previous test runs just once
// can run by itself using "it.only"
it('continues working with data created in the previous test', () => {
cy.dataSession('project name').then((projectName) => {
expect(projectName, 'got project name').to.be.a(
'string',
'project name created by previous test',
)
cy.wrap(projectName, { timeout: 0 }).should('include', 'project')
})
})

Store data using cypress-data-session plugin

Ok, let's see the second test by itself

1
2
3
4
5
6
7
8
9
it.only('continues working with data created in the previous test', () => {
cy.dataSession('project name').then((projectName) => {
expect(projectName, 'got project name').to.be.a(
'string',
'project name created by previous test',
)
cy.wrap(projectName, { timeout: 0 }).should('include', 'project')
})
})

The data is retrieved in the exclusive test

Ok, let's do hard browser reload.

The data is retrieved even after hard browser reload

Nice - this is it.

A better way

Is there a better way to write this spec to avoid dependency between the tests? Yes, you can even reuse the same data - but each test will create it if necessary:

cypress/e2e/spec4.cy.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
import 'cypress-data-session'

function createProjectIfNeeded() {
// how do we pass the data created in this test
// to the next test?
const projectName = `my random project ${Cypress._.random(1e4)}`

return cy.dataSession({
name: 'project name',
setup() {
return projectName
},
shareAcrossSpecs: true,
})
}

it('creates item A', () => {
cy.log('a very long test')
cy.wait(1000)
cy.log('that creates project')
createProjectIfNeeded().then((projectName) => {
cy.log(projectName)
})
})

// this test after the previous test runs just once
// can run by itself using "it.only"
it('continues working with data created in the previous test', () => {
createProjectIfNeeded().then((projectName) => {
expect(projectName, 'got project name').to.be.a(
'string',
'project name created by previous test',
)
cy.wrap(projectName, { timeout: 0 }).should('include', 'project')
})
})

When the tests run together, the project data is created by the first test and reused by the second one

The data is shared when the two tests run together

If we run the second test exclusively, the same project is fetched from the plugins process automatically.

The data is fetched by the second test

Finally, if we do hard reload, the data is loaded again from the plugins process.

The data survives hard browser reload

The two tests are independent from each other, yet the data is cached.

See also

A better approach to make independent fast tests is described in the post Change E2E Tests From UI To API To App Actions.