The Zen Of Cypress Data Setup

How to prepare the setup data without needing async / await in your specs.

When people talk about asynchronous programming, there are usually 3 "A"s involved:

  • async
  • await
  • anger

I get it. When I don't know how to do something, and the tools I am used to do not work, I get frustrated, and maybe even mad a little. Here is a solution: find a working example that shows how to do the thing you want to do. I have 500+ tested Cypress examples and recipes at glebbahmutov.com/cypress-examples, plus 400+ recorded Cypress videos, plus 250+ Cypress blog posts here. You can search them all from cypress.tips/search page. In this blog post, I will show how you can set up your test data before the test without getting into a dreaded Cypress Pyramid of Doom.

The app

You can find the example source code in the repo bahmutov/cypress-async-example. The application lets you add users and todos. Here is a test that adds 3 users "Joe", "Anna", and "Mary", and then adds 3 todos for the second user "Anna"

cypress/e2e/add-todos.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
it('adds todos for the user', () => {
cy.visit('/')
cy.get('#users tbody[loaded=true]')
cy.get('#new-user').type('Joe{enter}').type('Anna{enter}').type('Mary{enter}')
cy.get('table#users tbody tr').should('have.length', 3)
cy.contains('#users tr', 'Anna').find('input[type=radio]').click()
cy.get('input#new-todo').should('be.visible').type(`one{enter}`)
cy.get('input#new-todo').should('have.value', '')
// add two more todos
cy.get('input#new-todo').type('second{enter}')
cy.get('input#new-todo').type('third{enter}')
cy.get('table#todos tbody tr').should('have.length', 3)
})

Unfortunately, this test works fine the first time, but fails after that. The data from the first run remains in the database, breaking the expected number of users assertion.

The second execution of the test fails

We need to clean up any leftover items remaining from other tests or manual testing before the test runs.

The data setup

Our data is stored on the backend behind a REST API. We only have to resources: users and todos. Typical routes:

  • GET /todos returns all todos
  • GET /users returns all users
  • GET /users/:id returns the user with the given id
  • GET /todos?userId=:id returns todos for the user with the given id

Our data setup can be written using cy.request command. Here is what we need to do:

  • get all users
  • for each user get their todos
  • delete each todo
  • delete the user

Here is my first implementation

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
beforeEach(() => {
// get all users
cy.request('/users')
.its('body')
.then((users) => {
if (users.length) {
// for each user get their todos
users.forEach((user) => {
cy.request(`/todos/?userId=${user.id}`)
.its('body')
.then((todos) => {
if (todos.length) {
// delete each todo
todos.forEach((todo) => {
cy.request('DELETE', `/todos/${todo.id}`)
})
}
})
.then(() => {
// delete the user
cy.request('DELETE', `/users/${user.id}`)
})
})
}
})
})

The cleanup code works. Here is me running this test 3 times in a row.

Async data setup

Ok, some people object to the code above calling it "promise hell". For me, the question of Hell's existence is eternally unresolved, so I disagree :) In any case, why can't we do the following to make the data "flow left" from each cy.request command?

1
2
3
4
5
6
7
8
9
10
beforeEach(async () => {
// get all users
const users = await cy.request('/users').its('body')
if (users.length) {
// for each user get their todos
for await (const user of users) {
const todos = await cy.request(`/todos/?userId=${user.id}`)
// ... etc
}
})

Ok, here is a secret. In this case, we are simply making network requests. You can replace your cy.request commands with fetch JSON calls - and convert the entire beforeEach callback to async / await syntax, if you prefer it. Here it is

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
beforeEach(async () => {
// get all users
const users = await (await fetch('/users')).json()
if (users.length) {
// for each user get their todos
for (const user of users) {
const todos = await (await fetch(`/todos/?userId=${user.id}`)).json()
if (todos.length) {
// delete each todo
for (const todo of todos) {
await fetch(`/todos/${todo.id}`, { method: 'DELETE' })
}
}
// delete the user
await fetch(`/users/${user.id}`, { method: 'DELETE' })
}
}
})

it('adds todos for the user', () => {
cy.visit('/')
...
})

Refactoring

You have probably noticed some unnecessary code lines in the above beforeEach code. For example, we don't need to check the length of the users and todos arrays. We simply iterate over them, so it does not matter if they have any items or not.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
beforeEach(async () => {
// get all users
const users = await (await fetch('/users')).json()
// for each user get their todos
for (const user of users) {
const todos = await (await fetch(`/todos/?userId=${user.id}`)).json()
// delete each todo
for (const todo of todos) {
await fetch(`/todos/${todo.id}`, { method: 'DELETE' })
}
// delete the user
await fetch(`/users/${user.id}`, { method: 'DELETE' })
}
})

We can also move out deleting the todos and the user object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function removeUser(user) {
const todos = await (await fetch(`/todos/?userId=${user.id}`)).json()
// delete each todo
for (const todo of todos) {
await fetch(`/todos/${todo.id}`, { method: 'DELETE' })
}
// delete the user
await fetch(`/users/${user.id}`, { method: 'DELETE' })
}

beforeEach(async () => {
// get all users
const users = await (await fetch('/users')).json()
// for each user get their todos
for (const user of users) {
await removeUser(user)
}
})

Cypress code calling async code

Ok. The beforeEach hook now looks overcomplicated. I mean, all we are doing here is fetching the list of users and then calling async function removeUser(user). Here is a trick: combine Cypress commands with async functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function removeUser(user) {
const todos = await (await fetch(`/todos/?userId=${user.id}`)).json()
// delete each todo
for (const todo of todos) {
await fetch(`/todos/${todo.id}`, { method: 'DELETE' })
}
// delete the user
await fetch(`/users/${user.id}`, { method: 'DELETE' })
}

beforeEach(() => {
// get all users
cy.request('/users').its('body').each(removeUser)
})

We are making a cy.request, yield the response body (JSON response is automatically parsed), and then use the cy.each to call the async removeUser function, passing each user from the list. Since async functions return Promises, cy.each just works, as I show in my cy.each examples.

Let's do it again! Let's call async function from Cypress code to iterate over the list of todos.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function removeTodo(todo) {
// delete each todo
await fetch(`/todos/${todo.id}`, { method: 'DELETE' })
}

function removeUser(user) {
cy.request(`/todos/?userId=${user.id}`).its('body').each(removeTodo)
// How do we delete the user HERE???
await fetch(`/users/${user.id}`, { method: 'DELETE' })
}

beforeEach(() => {
// get all users
cy.request('/users').its('body').each(removeUser)
})

Hmm, we seem to hit a tiny snag. In the middle removeUser function, we need to grab the todos and remove the, but then we need to delete the user itself. Ughh, how do we combine Cypress code with async functions in the same block?

1
2
3
4
5
function removeUser(user) {
cy.request(`/todos/?userId=${user.id}`).its('body').each(removeTodo)
// How do we delete the user HERE???
await fetch(`/users/${user.id}`, { method: 'DELETE' })
}

Pretty simple. Not just cy.each(async callback) waits for the returned Promise, cy.then does too! We can write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function removeTodo(todo) {
// delete each todo
await fetch(`/todos/${todo.id}`, { method: 'DELETE' })
}

function removeUser(user) {
cy.request(`/todos/?userId=${user.id}`)
.its('body')
.each(removeTodo)
.then(async () => {
// delete the user
await fetch(`/users/${user.id}`, { method: 'DELETE' })
})
}

beforeEach(() => {
// get all users
cy.request('/users').its('body').each(removeUser)
})

More refactoring

Do you see it?

1
2
3
4
5
6
7
cy.request(`/todos/?userId=${user.id}`)
.its('body')
.each(removeTodo)
.then(async () => {
// delete the user
await fetch(`/users/${user.id}`, { method: 'DELETE' })
})

Can be written simply as:

1
2
3
4
5
cy.request(`/todos/?userId=${user.id}`)
.its('body')
.each(removeTodo)
// delete the user
.then(() => fetch(`/users/${user.id}`, { method: 'DELETE' }))

Ok, you know what is even simpler than .then? Using a regular Cypress command, because they are all automatically are placed into a queue and run one at a time. The above code can be written simply like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function removeTodo(todo) {
// delete each todo
await fetch(`/todos/${todo.id}`, { method: 'DELETE' })
}

function removeUser(user) {
cy.request(`/todos/?userId=${user.id}`).its('body').each(removeTodo)
// delete the user
cy.request('DELETE', `/users/${user.id}`)
}

beforeEach(() => {
// get all users
cy.request('/users').its('body').each(removeUser)
})

Do you see the next step? The last async function removeTodo is simply cy.request too:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function removeTodo(todo) {
// delete each todo
cy.request('DELETE', `/todos/${todo.id}`)
}

function removeUser(user) {
cy.request(`/todos/?userId=${user.id}`).its('body').each(removeTodo)
// delete the user
cy.request('DELETE', `/users/${user.id}`)
}

beforeEach(() => {
// get all users
cy.request('/users').its('body').each(removeUser)
})

All commands are by their nature asynchronous. Cypress commands are so polite, like British people, the commands will naturally queue up and will never try to skip the line when you forget to put "await" in front of each and every one of them.

A better way

Do I use this approach to set up the data before the test? Sometimes I do. But in 90% of the time I use something very different. I usually have some API or way to access the backend database directly from my tests. For example, in this application, I am using json-server to create the REST API. I have a little plugin json-server-reset to reset the data when running in DEV mode.

My npm start command loads the middleware when I do Cypress end-to-end testing

1
2
3
$ json-server --port 3300 --static public \
data.json \
--middleware ./node_modules/json-server-reset

The middleware adds POST /reset endpoint where I can send the clean data I want right away. I can empty both resources:

1
2
3
4
beforeEach(() => {
// empty everything
cy.request('POST', '/reset', { users: [], todos: [] })
})

A single cy.request command cleans everything up

One quick move and the backend is ready. I could import fixtures and start with 3 users right away. To avoid a tiny "pyramid" of Doom, I could either import JSON fixture files or load them in the separate beforeEach hooks and use alias (read Import Cypress fixtures for the full details)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
beforeEach(() => {
cy.fixture('three-users.json').as('users')
})

beforeEach(function () {
// empty everything
cy.request('POST', '/reset', { users: this.users, todos: [] })
})

it('adds todos for the user', () => {
cy.visit('/')
cy.get('#users tbody[loaded=true]')
cy.get('table#users tbody tr').should('have.length', 3)
cy.contains('#users tr', 'Anna').find('input[type=radio]').click()
cy.get('input#new-todo').should('be.visible').type(`one{enter}`)
cy.get('input#new-todo').should('have.value', '')
// add two more todos
cy.get('input#new-todo').type('second{enter}')
cy.get('input#new-todo').type('third{enter}')
cy.get('table#todos tbody tr').should('have.length', 3)
})

A single cy.request command sets 3 users and 0 todos

Fast and simple!