Refactor Cypress Network Tests

An example improving two Cypress tests that stub the network calls.

Recently I have seen an example screenshot showing two Cypress tests using the great cy.intercept command to test the loading message and error handling. You can see the original post on twitter and linkedin. Here is the screenshot:

Two Cypress tests using cy.intercept command

Let's improve these tests a little.

Set network spy before the action

The first improvement we are going to make is to remove a potential flake in the first test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// cypress/e2e/bonus134.cy.js

describe('Page loads slowly', () => {
beforeEach(() => {
cy.visit('/loading.html')
})

it('shows the loading element', () => {
cy.intercept('GET', '/fruit', (req) => {
req.on('response', (res) => {
res.setDelay(1500) // 1.5 seconds delay
})
}).as('fetchFruit')

cy.get('div').contains('loading').should('be.visible')
cy.wait('@fetchFruit')
cy.get('div').contains('loading').should('not.exist')
})
})

The test might pass, but it does have a problem

The first test passes

Do you see the potential problem in this test? If not, read the "The intercept was registered too late" in the blog post Cypress cy.intercept Problems. We can see the error if we modify our application code and make the loading call start sooner.

The first test fails if the network call happens quickly

We should always register the network intercept before the application makes the call. I will move the cy.visit into the test and place it after the cy.intercept command.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
it('shows the loading element', () => {
cy.intercept('GET', '/fruit', (req) => {
req.on('response', (res) => {
res.setDelay(1500) // 1.5 seconds delay
})
}).as('fetchFruit')
// once the network intercept is set up
// visit the page which kicks off the request
cy.visit('/loading.html')

cy.get('div').contains('loading').should('be.visible')
cy.wait('@fetchFruit')
cy.get('div').contains('loading').should('not.exist')
})

The above test is robust and should always work.

Checking the loading element

I noticed a lot of people using cy.get(selector).contains(text) command. The cy.contains command already accepts the cy.contains(selector, text) arguments, no need to chain it off a cy.get in most cases. The above test can be rewritten as:

1
2
3
4
5
6
7
8
9
10
11
12
it('shows the loading element (2)', () => {
cy.intercept('GET', '/fruit', (req) => {
req.on('response', (res) => {
res.setDelay(1500) // 1.5 seconds delay
})
}).as('fetchFruit')
cy.visit('/loading.html')

cy.contains('#fruit', 'loading').should('be.visible')
cy.wait('@fetchFruit')
cy.get('#fruit').should('not.contain', 'loading')
})

Tip: we could make the test even stronger by checking if the #fruit element shows the fruit returned by the server.

Delay the response

In our test, we simply spy on the network call. The call still goes to the server, which returns the real data. We simply delay sending the response to the browser by 1.5 seconds. This can be written simply as:

1
2
3
4
5
6
7
8
9
10
it('shows the loading element (3)', () => {
cy.intercept('GET', '/fruit', () =>
Cypress.Promise.delay(1500),
).as('fetchFruit')
cy.visit('/loading.html')

cy.contains('#fruit', 'loading').should('be.visible')
cy.wait('@fetchFruit')
cy.get('#fruit').should('not.contain', 'loading')
})

We are delaying sending the request to the server by 1.5 seconds, which is the same for our test.

The second test

The second test shown in the original image tests an error scenario. We stub the network call and return status code 500. Tip: when stubbing the network call, we can set the delay right away.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// before 👎
beforeEach(() => {
cy.visit('/loading.html')
})

it('should display error message', () => {
cy.intercept('GET', '/fruit', {
statusCode: 500,
}).as('fetchFruit')
cy.wait('@fetchFruit')
cy.contains('HTTP error 500')
})

// after ✅
it('should display error message', () => {
cy.intercept('GET', '/fruit', {
statusCode: 500,
}).as('fetchFruit')
cy.visit('/loading.html')
cy.wait('@fetchFruit')
cy.contains('HTTP error 500')
})

Testing the application network error handling

Tip: when stubbing network calls using cy.intercept, it is very easy to delay the response using the built-in response stub object property delay

1
2
3
4
5
6
7
8
9
10
11
12
13
it('should display error message after a delay', () => {
cy.intercept('GET', '/fruit', {
statusCode: 500,
delay: 1500,
}).as('fetchFruit')
cy.visit('/loading.html')
cy.contains('#fruit', 'loading').should('be.visible')
cy.wait('@fetchFruit')
// the error message should be displayed really quickly
cy.contains('#fruit', 'HTTP error 500', {
timeout: 0,
}).should('be.visible')
})

Testing the application loading state and error handling

🎓 All examples shown in this blog post are covered by my hands-on course Cypress Network Testing Exercises.