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"
1 | it('adds todos for the user', () => { |
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.
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 todosGET /users
returns all usersGET /users/:id
returns the user with the given idGET /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 | beforeEach(() => { |
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 | beforeEach(async () => { |
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 | beforeEach(async () => { |
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 | beforeEach(async () => { |
We can also move out deleting the todos and the user object
1 | async function 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 | async function removeUser(user) { |
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 | async function removeTodo(todo) { |
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 | function removeUser(user) { |
Pretty simple. Not just cy.each(async callback)
waits for the returned Promise, cy.then does too! We can write
1 | async function removeTodo(todo) { |
More refactoring
Do you see it?
1 | cy.request(`/todos/?userId=${user.id}`) |
Can be written simply as:
1 | cy.request(`/todos/?userId=${user.id}`) |
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 | async function removeTodo(todo) { |
Do you see the next step? The last async function removeTodo
is simply cy.request
too:
1 | function removeTodo(todo) { |
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 | $ json-server --port 3300 --static public \ |
The middleware adds POST /reset
endpoint where I can send the clean data I want right away. I can empty both resources:
1 | beforeEach(() => { |
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 | beforeEach(() => { |
Fast and simple!