How To Check Network Requests Using Cypress

How to validate the network intercept using Cypress tests.

Recently a user posted in the Cypress Gitter channel the following image and asked why this code is not working.

This code is broken

The above code has several problems. In this blog post, I will fix them all. I have recorded a short video showing the fixes step by step, you can watch the video below, or keep reading.

The returned value

First, let's deal with the returned value of the cy.wait(...).then(...) chain.

The returned value is not the organization id

Think about JavaScript promises. A promise .then method does NOT return the value, it returns another promise, so you can attach another .then callback, or a callback to catch an error using .catch, right?

1
2
// INCORRECT, the "x" is a promise instance, not the value 42
const x = Promise.resolve(...).then(...).then(() => 42)

If you want to get the resolved value x, you need to put the code that uses x into the last callback.

1
2
3
4
5
// FIXED, the "x" is used inside `.then(...)` callback
Promise.resolve(...).then(...).then(() => 42)
.then(x => {
// use x here, x should be 42
})

Cypress command chains might look like promise chains, but they are more like reactive streams, see this presentation from ReactiveConf 2018. Thus every command like cy.wait and cy.then returns another instance of Cypress chainable interface so you can attach more commands. If you want to use the value returned (or "yielded" as Cypress calls it), use it inside .then(...) callback.

1
2
3
4
5
6
7
8
9
10
cy.wait('@createUnion').then(response => {
const { statusCode, body } = response.response
const org = body.data

// validate the response

return org.id
}).then(orgId => {
// you can use the orgId now
})

Printing the value

Let's look at why the console.log(orgId) prints a weird object.

The console.log prints something unexpected

As I explained above, the returned value of the Cypress callback is an internal chainable object used to add more commands to be executed. The Cypress commands themselves are queued up, the have not started running. The Cypress chains of commands are lazy - they only start running once the browser is ready (which is another difference from the Promises which are very very eager to run). If we look at the order of execution, the console.log runs way before the Cypress command gets the intercepted response and gets the ID property.

I marked the order of statements executed in the code snippet.

The order in which the code statements run

  1. First, the code runs cy.wait to schedule the "WAIT" command. It returns Cypress chainable object
  2. The Cypress chainable object has then method, it gets called with a function callback reference. The Cypress method schedules the "THEN" command to be run after "WAIT" is finished (nothing is running at this point).
  3. There are no more Cypress commands to call, thus the assignment const orgId = runs, assigning the (unexpected) Cypress chainable object reference to the local variable orgId
  4. The problem happens here: the next JavaScript statement that runs is the console.log(orgId) which tries to print the Chainable object, while the user expects to see the organization id. So it prints something weird.
  5. Cypress test starts running, finds the "WAIT" command, waits for that alias "@createUnion", yields the intercept to the next scheduled command "THEN", calls the function callback which returns the real organization ID.

To fix the code snippet, move the console.log into the .then callback that gets the ID, or attach it as another .then callback.

1
2
3
4
5
6
7
8
cy.wait('@createUnion').then(response => {
const { statusCode, body } = response.response
const org = body.data

// validate the response

return org.id
}).then(console.log)

Tip: you can print the value to the DevTools using .then(console.log) but a better idea is to print it to the Cypress Command Log with .then(cy.log). See the video How to use Cypress cy.log command to output messages to the Command Log.

In general, anything you get from the application page, or from another Cypress command must be used inside the .then callback to have its value set. I have two short videos explaining the above problem and how to write your tests correctly.

Why cy.log prints null or undefined

When To Use Cypress .Then Callback To Use The Value

Fluent programming

Now let's refactor the body of the .then(...) callback function. Currently it takes the response object from the intercept, runs multiple assertions against it, then yields the organization ID. Let's refactor it for clarity.

🎓 You can find the code from this section in my workshop "Cypress Basics" in the repo bahmutov/cypress-workshop-basics.

Our first attempt mimics the user's question.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
it('validates and processes the intercept object', () => {
cy.intercept('POST', '/todos').as('postTodo')
const title = 'new todo'
const completed = false
cy.get('.new-todo').type(title + '{enter}')
cy.wait('@postTodo')
.then((intercept) => {
// get the field from the intercept object
const { statusCode, body } = intercept.response
// confirm the status code is 201
expect(statusCode).to.eq(201)
// confirm some properties of the response data
expect(body.title).to.equal(title)
expect(body.completed).to.equal(completed)
// return the field from the body object
return body.id
})
.then(cy.log)
})

The test is green.

The test validates the response

Let's look at the code. First, we are only interested in the property response from the intercept (there are a lot more!). Thus let's extract it using cy.its command.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
it('extracts the response property first', () => {
cy.intercept('POST', '/todos').as('postTodo')
const title = 'new todo'
const completed = false
cy.get('.new-todo').type(title + '{enter}')
cy.wait('@postTodo')
.its('response')
.then((response) => {
const { statusCode, body } = response
// confirm the status code is 201
expect(statusCode).to.eq(201)
// confirm some properties of the response data
expect(body.title).to.equal(title)
expect(body.completed).to.equal(completed)
// return the field from the body object
return body.id
})
.then(cy.log)
})

It runs the same way. If you need to debug the intercept object, click on the "ITS" command and see it in the DevTools console.

Print the entire intercept object to the DevTools console

Let's look at our assertions. Right now they offer very little to the developer aside from printing their value. Let's add a message to each assertion to make it clearer.

1
2
3
4
5
.then((response) => {
const { statusCode, body } = response
// confirm the status code is 201
expect(statusCode, 'status code').to.eq(201)
})

Added message argument to the assertion

Isn't the top assertion more informative than the next two? We can make it even better by using the specific Chai assertion for checking the property.

1
2
3
4
5
.then((response) => {
const { body } = response
// confirm the status code is 201
expect(response).to.have.property('statusCode', 201)
})

The property assertion has even more information

If Cypress .then command returns undefined and has no other Cypress commands, then its original subject value gets passed to the next command automatically. Thus we can move the statusCode check into its own .then callback to separate it from the response object checks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cy.wait('@postTodo')
.its('response')
.then((response) => {
// confirm the status code is 201
expect(response).to.have.property('statusCode', 201)
})
.then((response) => {
const { body } = response
// confirm some properties of the response data
expect(body.title).to.equal(title)
expect(body.completed).to.equal(completed)
// return the field from the body object
return body.id
})

The output looks the same as before, but now we can notice that we only deal with the body property from the response object. Let's apply cy.its command, just like we extracted the response from the intercept object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cy.wait('@postTodo')
.its('response')
.then((response) => {
// confirm the status code is 201
expect(response).to.have.property('statusCode', 201)
})
.its('body')
.then((body) => {
// confirm some properties of the response data
expect(body.title).to.equal(title)
expect(body.completed).to.equal(completed)
// return the field from the body object
return body.id
})
.then(cy.log)

Using the body from the response

Remember - the cy.its command automatically fails if the property does not exist on the object. It also accepts nested properties, so you could grab the body from the intercept object using cy.wait(...).its('response.body') syntax.

Now the last callback only deals with the properties of a single body object. We can confirm some of the properties using deep.include assertion.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cy.wait('@postTodo')
.its('response')
.then((response) => {
// confirm the status code is 201
expect(response).to.have.property('statusCode', 201)
})
.its('body')
.then((body) => {
// confirm some properties of the response data
expect(body).to.deep.include({
title,
completed
})
// return the field from the body object
return body.id
})

We cannot use deep.equals since we do not know the "id" property. For more assertion examples, see my Assertions page.

Ok, so returning body.id at the end could use cy.its command, so let's move it out. Since we are not returning anything from the .then(body => ...) callback, the body wil be yielded to the next command.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cy.wait('@postTodo')
.its('response')
.then((response) => {
// confirm the status code is 201
expect(response).to.have.property('statusCode', 201)
})
.its('body')
.then((body) => {
// confirm some properties of the response data
expect(body).to.deep.include({
title,
completed
})
})
.its('id')
.then(cy.log)

We now have just the assertions inside .then callback. Thus we can use the BDD should assertion instead.

1
2
3
4
5
6
7
8
9
10
cy.wait('@postTodo')
.its('response')
.then((response) => {
// confirm the status code is 201
expect(response).to.have.property('statusCode', 201)
})
.its('body')
.should('deep.include', { title, completed })
.its('id')
.then(cy.log)

BDD should assertion

I hope the above test code transformation has shown some of the beauty and power of the Cypress fluent syntax where we can chain the commands and the assertions.

Aside: we cannot use the BDD should assertion to verify the status code property like should('have.property', 'statusCode', 201) fluent syntax because have.property is one of just a few assertions that change the subject to the property value, while we need to keep the response object.

Bonus: cy-spok

The test looks good, but there is one improvement we can make. I like the Command Log to be as useful as possible. The standard Chai assertions are good, but cy-spok makes them perfect.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import spok from 'cy-spok'
cy.wait('@postTodo')
.its('response')
.should(
spok({
statusCode: 201
})
)
.its('body')
.should(
spok({
title,
completed
})
)
.its('id')
.then(cy.log)

Look at the Command Log output - isn't this super helpful?

Using cy-spok to write assertions

The plugin cy-spok is built on top of spok which is really good at asserting nested objects and even checking built-in predicates. For example, we do not know the id of the item, but we know it is a string. Let's write a single assertion to verify the properties we can inside the entire intercept object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import spok from 'cy-spok'
it('checks the response using cy-spok', () => {
cy.intercept('POST', '/todos').as('postTodo')
const title = 'new todo'
const completed = false
cy.get('.new-todo').type(title + '{enter}')
cy.wait('@postTodo')
.its('response')
.should(
spok({
statusCode: 201,
body: {
title,
completed,
id: spok.string
}
})
)
.its('body.id')
.then(cy.log)
})

Using cy-spok to verify the entire nested object

Love it.