Load Fixtures from Cypress Custom Commands

How to load or import fixtures to be used in the Cypress custom commands

This blog post multiples ways to pick a random item from a fixture file, and then reuse that picked item in multiple Cypress tests. We will look at fixtures, aliases, hooks, and custom commands.

The application

Let's take a Cypress test verifies the application loads and displays the user name:

cypress/integration/spec.js
1
2
3
4
5
6
/// <reference types="cypress" />

it('loads the first user', () => {
cy.visit('/')
cy.contains('#user', 'Joe Smith')
})

The passing test

We can inspect the application code to see how to fetches the user, it fetches it from the REST resource on load:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fetch('/users/1')
.then(r => {
if (r.ok) {
return r.json()
} else {
throw new Error(r.statusText)
}
})
.then(user => {
document.getElementById('user').innerText =
`${user.name.first} ${user.name.last}`
})
.catch(e => {
document.getElementById('user').innerText = e.message
})

📦 You can find the application and the test for this blog post at bahmutov/cy-user-register-example repository.

The database

Our application has a very simply database implemented using json-server utility. It servers the REST resources from the db.json file. Currently we have a single hard-coded user object there.

db.json
1
2
3
4
5
6
7
8
9
{
"users": [{
"id": 1,
"name": {
"first": "Joe",
"last": "Smith"
}
}]
}

Having a single permanent user is no fun, what is the point of that? So let's reset the database /users list before each test, and instead place our test user there. Then we can verify the test user is displayed on load. We are including the json-server-reset middleware module for this:

package.json
1
2
3
4
5
{
"scripts": {
"start": "json-server --port 6001 --watch db.json --middlewares ./node_modules/json-server-reset"
}
}

From the test we can send the POST /reset HTTP message using the cy.request command.

cypress/integration/spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <reference types="cypress" />

it('loads the test user', () => {
const testUser = {
id: 1,
name: {
first: 'Testing',
last: 'Expert',
},
}
// we need to send the entire database object
cy.request('POST', '/reset', {
users: [testUser],
})
cy.visit('/')
cy.contains('#user', `${testUser.name.first} ${testUser.name.last}`)
})

We can see the successful test and the reset message from the server in the terminal

Posting and verifying using the test user

Picking a random user

It is no fun to always see the same one user. Let's have a list of users to test with instead. We can pick a random user from the list when starting the test. Here is our fixture file: notice that every user has the same id 1 - because that will be the resource id later

cypress/fixtures/data.json
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
{
"users": [
{
"id": 1,
"name": {
"first": "Joe",
"last": "Smith"
}
},
{
"id": 1,
"name": {
"first": "Mary",
"last": "Adams"
}
},
{
"id": 1,
"name": {
"first": "Mike",
"last": "Valente"
}
}
]
}

Our test can load the entire object from the cypress/fixtures/data.json file using the cy.fixture command, then pick a random user and send the POST /reset request.

cypress/integration/spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <reference types="cypress" />

it('sets the random user from the fixture list', () => {
cy.fixture('data.json').then(({ users }) => {
// the index k will be from 0 to users.length - 1
const k = Cypress._.random(users.length - 1)
expect(k, 'random user index').to.be.within(0, users.length - 1)
const testUser = users[k]

// we need to send the entire database object
cy.request('POST', '/reset', {
users: [testUser],
})
cy.visit('/')
cy.contains('#user', `${testUser.name.first} ${testUser.name.last}`)
})
})

If we run the test several times, it shows a different user.

Testing with a randomly picked user from the fixture

Using a before hook

If we have several tests, we do not need to reset the user inside each test. Instead we can reset the test once using the before hook. We just need to decide how to pass the picked testUser object from the hook to the test, because it needs the name to find on the page.

cypress/integration/spec.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
/// <reference types="cypress" />

before(() => {
cy.fixture('data.json').then(({ users }) => {
// the index k will be from 0 to users.length - 1
const k = Cypress._.random(users.length - 1)
expect(k, 'random user index').to.be.within(0, users.length - 1)
const testUser = users[k]
// save the picked user using an alias
cy.wrap(testUser).as('testUser')

// we need to send the entire database object
cy.request('POST', '/reset', {
users: [testUser],
})
})
})

it('sets the random user from the fixture list', function () {
cy.visit('/')
// because we used the "function () { ... }" callback
// the "this" points at the test context
// where the "testUser" property was already set
// by the "before" hook via ".as('testUser')"
const name = this.testUser.name
cy.contains('#user', `${name.first} ${name.last}`)
})

The test passes, we can see the alias set using the .as command.

Setting the picked user in the before hook

Problem: aliases are reset

Unfortunately this implementation has a problem: all aliases are reset before each test. If we add another test that needs the testUser object, it would fail.

cypress/integration/spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
it('sets the random user from the fixture list', function () {
cy.visit('/')
// because we used the "function () { ... }" callback
// the "this" points at the test context
// where the "testUser" property was already set
// by the "before" hook via ".as('testUser')"
const name = this.testUser.name
cy.contains('#user', `${name.first} ${name.last}`)
})

it('has the test user', function () {
expect(this.testUser).to.be.an('object')
})

The alias testUser is unavailable after the first test

Use a common variable

We solve this problem in several ways - it is just JavaScript, after all. For example, we could store the test user reference in a variable in the shared lexical scope bypassing the alias.

cypress/integration/spec.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
/// <reference types="cypress" />

// use a common variable to store the random user
let testUser

before(() => {
cy.fixture('data.json').then(({ users }) => {
// the index k will be from 0 to users.length - 1
const k = Cypress._.random(users.length - 1)
expect(k, 'random user index').to.be.within(0, users.length - 1)
testUser = users[k]

// we need to send the entire database object
cy.request('POST', '/reset', {
users: [testUser],
})
})
})

it('sets the random user from the fixture list', () => {
cy.visit('/')
const name = testUser.name
cy.contains('#user', `${name.first} ${name.last}`)
})

it('has the test user', () => {
expect(testUser).to.be.an('object')
})

The tests work

Using a shared variable to pass the picked user

Use before and beforeEach hooks

We can still use the hooks and aliases. We can pick the user once, and then reset the database and set the alias before each test using the before + beforeEach hook combination.

cypress/integration/spec.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
/// <reference types="cypress" />

// use a common variable to store the random user
let testUser

before(() => {
cy.fixture('data.json').then(({ users }) => {
// the index k will be from 0 to users.length - 1
const k = Cypress._.random(users.length - 1)
expect(k, 'random user index').to.be.within(0, users.length - 1)
testUser = users[k]
})
})

beforeEach(() => {
cy.wrap(testUser).as('testUser')

// we need to send the entire database object
cy.request('POST', '/reset', {
users: [testUser],
})
})

it('sets the random user from the fixture list', function () {
cy.visit('/')
const name = testUser.name
cy.contains('#user', `${name.first} ${name.last}`)
})

it('has the test user', function () {
expect(this.testUser).to.be.an('object')
})

The tests pass, the user is generated once in the before hook, and the beforeEach hooks reset the database to that user, and set the alias to be available in every test.

Resetting the alias using the beforeEach hook

Use the Cypress.config method

There is another way to store and pass a value: Cypress.config command. Under the hood it is nothing but a plain object, and we can store any values there. Who is going to stop us? There is no Cypress police to tell the users what to do.

cypress/integration/spec.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
before(() => {
cy.fixture('data.json').then(({ users }) => {
// the index k will be from 0 to users.length - 1
const k = Cypress._.random(users.length - 1)
expect(k, 'random user index').to.be.within(0, users.length - 1)
Cypress.config('testUser', users[k])
})
})

beforeEach(() => {
const testUser = Cypress.config('testUser')
// we need to send the entire database object
cy.request('POST', '/reset', {
users: [testUser],
})
})

it('sets the random user from the fixture list', () => {
const testUser = Cypress.config('testUser')
cy.visit('/')
const name = testUser.name
cy.contains('#user', `${name.first} ${name.last}`)
})

it('has the test user', () => {
const testUser = Cypress.config('testUser')
expect(testUser).to.be.an('object')
})

Use the Cypress.env method

I prefer a different method for storing and retrieving my own values - Cypress.env. It works exactly like Cypress.config under the hood - just a plain object to store your properties.

cypress/integration/spec.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
/// <reference types="cypress" />

before(() => {
cy.fixture('data.json').then(({ users }) => {
// the index k will be from 0 to users.length - 1
const k = Cypress._.random(users.length - 1)
expect(k, 'random user index').to.be.within(0, users.length - 1)
Cypress.env('testUser', users[k])
})
})

beforeEach(() => {
const testUser = Cypress.env('testUser')
// we need to send the entire database object
cy.request('POST', '/reset', {
users: [testUser],
})
})

it('sets the random user from the fixture list', () => {
const testUser = Cypress.env('testUser')
cy.visit('/')
const name = testUser.name
cy.contains('#user', `${name.first} ${name.last}`)
})

it('has the test user', () => {
const testUser = Cypress.env('testUser')
expect(testUser).to.be.an('object')
})

Custom command

If we always want to pick a random user, might as well make it into a reusable command. First, let's write a single custom command to pick a user and reset the database.

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
/// <reference types="cypress" />

Cypress.Commands.add('pickTestUser', () => {
cy.fixture('data.json').then(({ users }) => {
// the index k will be from 0 to users.length - 1
const k = Cypress._.random(users.length - 1)
expect(k, 'random user index').to.be.within(0, users.length - 1)
Cypress.env('testUser', users[k])
})
})

before(() => {
cy.pickTestUser()
})

beforeEach(() => {
const testUser = Cypress.env('testUser')
// we need to send the entire database object
cy.request('POST', '/reset', {
users: [testUser],
})
})

it('sets the random user from the fixture list', () => {
const testUser = Cypress.env('testUser')
cy.visit('/')
const name = testUser.name
cy.contains('#user', `${name.first} ${name.last}`)
})

it('has the test user', () => {
const testUser = Cypress.env('testUser')
expect(testUser).to.be.an('object')
})

Using Cypress.env to store data has one small advantage: at the end of the test or while pausing it, you can always fetch the current value by opening the DevTools console and simply running Cypress.env('testUser') command - all methods on the Cypress object are always available and can be executed at any time.

Log the current user using DevTools

Using the memo pattern

Picking the random user does not need to use Cypress.env or other methods to yield the testUser object. Instead it can use a memo pattern. If the value does not exist (we can use a local variable for example), then we can generate the random user and remember it. Next time, we can simply return that value.

cypress/integration/spec.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
39
40
41
/// <reference types="cypress" />

// local variable to remember the once generated user
let testUser

Cypress.Commands.add('pickTestUser', () => {
if (testUser) {
return cy.wrap(testUser)
}

cy.fixture('data.json').then(({ users }) => {
// the index k will be from 0 to users.length - 1
const k = Cypress._.random(users.length - 1)
expect(k, 'random user index').to.be.within(0, users.length - 1)
testUser = users[k]
// yield the generated test user object
cy.wrap(testUser)
})
})

beforeEach(() => {
cy.pickTestUser().then((testUser) => {
// we need to send the entire database object
cy.request('POST', '/reset', {
users: [testUser],
})
})
})

it('sets the random user from the fixture list', () => {
cy.pickTestUser().then((testUser) => {
cy.visit('/')
const name = testUser.name
cy.contains('#user', `${name.first} ${name.last}`)
})
})

it('has the test user', () => {
// use .should assertion on the yielded object
cy.pickTestUser().should('be.an', 'object')
})

If you inspect the Command Log, you can confirm that the random user was indeed picked once, and then the same user was yielded before each test.

Memo pattern for the picked user

Support file

Since the custom command and the tests do not use any shared variables to pass the data, we can safely move the custom command to the Cypress support file. This will make the new command available in every spec.

cypress/support/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <reference types="cypress" />

// local variable to remember the once generated user
let testUser

Cypress.Commands.add('pickTestUser', () => {
if (testUser) {
return cy.wrap(testUser)
}

cy.fixture('data.json').then(({ users }) => {
// the index k will be from 0 to users.length - 1
const k = Cypress._.random(users.length - 1)
expect(k, 'random user index').to.be.within(0, users.length - 1)
testUser = users[k]
// yield the generated test user object
cy.wrap(testUser)
})
})
cypress/integration/spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// <reference types="cypress" />

beforeEach(() => {
cy.pickTestUser().then((testUser) => {
// we need to send the entire database object
cy.request('POST', '/reset', {
users: [testUser],
})
})
})

it('sets the random user from the fixture list', () => {
cy.pickTestUser().then((testUser) => {
cy.visit('/')
const name = testUser.name
cy.contains('#user', `${name.first} ${name.last}`)
})
})

it('has the test user', () => {
// use .should assertion on the yielded object
cy.pickTestUser().should('be.an', 'object')
})

Note: since we have just one custom command, I simply placed it into the cypress/support/index.js file. If we had many custom commands, I would have them in their own JS files and would import them from the support file.

Use imports

Our list of users comes from the JSON fixture file. We can directly import or require this JSON file from the JavaScript files, our bundler knows how to load a JSON object. Thus picking of the test user could be rewritten as:

cypress/support/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
/// <reference types="cypress" />

import { users } from '../fixtures/data.json'

// note that Cypress._ is available outside of any test.
// the index k will be from 0 to users.length - 1
const k = Cypress._.random(users.length - 1)
expect(k, 'random user index').to.be.within(0, users.length - 1)
const testUser = users[k]

Cypress.Commands.add('pickTestUser', () => {
cy.wrap(testUser)
})

The test works the same way as before (well, almost the same way).

Importing the fixture file to pick the user

Notice the difference: we had an expect(k, 'random user index) assertion outside of any test. Such assertions when passing do NOT show up in the Command Log at all. If they fail, Cypress does show them. Let's modify the assertion to make it fail to see it:

1
expect(k, 'random user index').to.be.within(100, users.length - 1)

Failed assertion outside a test

Nice!

See also