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.
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.
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:
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
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
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.
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.
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.
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') })
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.
// 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
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.
// 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', 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.
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.
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.
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.
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.
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.
// 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.
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) }) })
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]
The test works the same way as before (well, almost the same way).
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)