This blog post tests an application that fetches new data every 30 seconds, but the test itself runs in milliseconds because it controls the application's clock and stubs the network responses.
Note: you can find the source code in the cy.intercept recipe in the repository cypress-example-recipes.
Application
Our example application fetches a list of fruits to display. It fetches them at startup and it fetches the list of fruits every 30 seconds after that.
Let's see how we can confirm this behavior.
Loaded fruits
First, let us confirm the application is showing the list of fruits returned by the server on startup. We need to spy on the initial request and check the displayed items. The cy.intercept command makes it simple to spy on any request.
it('requests favorite fruits', function () {
cy.intercept('/favorite-fruits').as('fetchFruits')
cy.visit('/fruits.html')
cy.wait('@fetchFruits').its('response.body')
.then((fruits) => {
cy.get('.favorite-fruits li')
.should('have.length', fruits.length)
fruits.forEach((fruit) => {
cy.contains('.favorite-fruits li', fruit)
})
})
})
The test waits for the first fetch
to complete, then grabs the response from the server and checks that each item is displayed on the page.
Great, can we confirm the application loads a new list 30 seconds later?
Very very slow
We can use cy.wait to have the Test Runner "sleep" for 30 seconds, and then can check the intercept and the page. Our slow test would look like this:
it('fetches fruits every 30 seconds', () => {
cy.intercept('/favorite-fruits').as('fetchFruits')
cy.visit('/fruits.html')
// confirm the first request happens
cy.wait('@fetchFruits')
// wait 30 seconds ...
cy.wait(30000)
// confirm the second request happens
cy.wait('@fetchFruits').its('response.body')
.then((fruits) => {
cy.get('.favorite-fruits li')
.should('have.length', fruits.length)
fruits.forEach((fruit) => {
cy.contains('.favorite-fruits li', fruit)
})
})
})
Of course, it takes 30 seconds to run this test even once.
Can we speed it up?
Control the clock
Cypress has commands to control the application's clock during the test. We can "freeze" the clock and all time-related functions like setInterval
, setTimeout
using the command cy.clock. Then we can manually advance the clock using the cy.tick command. Here is our much faster test:
it('fetches from the server (spies)', () => {
cy.clock()
cy.intercept('GET', '/favorite-fruits').as('fruits')
cy.visit('/fruits.html')
// first call
cy.wait('@fruits').its('response.statusCode').should('equal', 200)
// 30 seconds passes and the application fetches again
cy.tick(30000)
cy.wait('@fruits').its('response.statusCode').should('equal', 200)
// 3rd call
cy.tick(30000)
cy.wait('@fruits').its('response.statusCode').should('equal', 200)
// 4th call
cy.tick(30000)
cy.wait('@fruits').its('response.statusCode').should('equal', 200)
// 5th call
cy.tick(30000)
cy.wait('@fruits').its('response.statusCode').should('equal', 200)
// confirm the displayed fruits
cy.get('@fruits').its('response.body')
.then((fruits) => {
cy.get('.favorite-fruits li')
.should('have.length', fruits.length)
fruits.forEach((fruit) => {
cy.contains('.favorite-fruits li', fruit)
})
})
})
The test "fast-forwards" 30 second intervals using the cy.tick(30000)
command, checking the intercept's status code. On the last 5th request, we grab the response and confirm the last list of fruits is shown on the page.
The entire test is eye-blinking fast: 310ms total. Nice!
Stubbing the server response
What if we do not have a server during testing? In that case instead of spying on the network requests, we can stub them. Thanks to cy.intercept
we can code the response handler to return different lists of fruits for different requests. For example, on the first request we will return apples, on the second request we will return grapes, and for every request after that we will return kiwi.
it('returns different fruits every 30 seconds', () => {
cy.clock()
let k = 0
// return difference responses on each call
cy.intercept('/favorite-fruits', (req) => {
k += 1
switch (k) {
case 1:
return req.reply(['apples 🍎'])
case 2:
return req.reply(['grapes 🍇'])
default:
return req.reply(['kiwi 🥝'])
}
})
cy.visit('/fruits.html')
cy.contains('apples 🍎')
cy.tick(30000)
cy.contains('grapes 🍇')
cy.tick(30000)
cy.contains('kiwi 🥝')
})
The test is simple and fast.
You can code the /favorite-fruits
stub to be as simple or advanced as you want. For example, the same test could be coded up using an array of responses.
it('returns different fruits every 30 seconds (array shift)', () => {
cy.clock()
// return difference responses on each call
const responses = [
['apples 🍎'], ['grapes 🍇'],
]
cy.intercept('/favorite-fruits', (req) => {
req.reply(responses.shift() || ['kiwi 🥝'])
})
cy.visit('/fruits.html')
cy.contains('apples 🍎')
cy.tick(30000)
cy.contains('grapes 🍇')
cy.tick(30000)
cy.contains('kiwi 🥝')
})
Every time the intercept runs, it uses up the first item from the responses
array and removes it. After the first two times, the responses.shift()
always returns undefined
and we return the default kiwi fruit.
Slowing down the test
The above tests are good, but they are perhaps too fast. For example I cannot even see the 3 different fruits displayed during the test.
Imagine trying to debug a failing test on CI from its video recording - you would not be able to tell what is going on - the test is too fast! Thus for such tests I recommend slowing it down on purpose. Of course, we do not have to wait 30 seconds between each network call. But 1 second waits would let us see what is going on during the test.
it('returns different fruits every 30 seconds (slow down)', () => {
cy.clock()
// return difference responses on each call
const responses = [
['apples 🍎'], ['grapes 🍇'],
]
cy.intercept('/favorite-fruits', (req) => {
req.reply(responses.shift() || ['kiwi 🥝'])
})
cy.visit('/fruits.html')
cy.contains('apples 🍎')
// slow down the test on purpose to be able to see
// the rendered page at each step
cy.wait(1000)
cy.tick(30000)
cy.contains('grapes 🍇')
cy.wait(1000)
cy.tick(30000)
cy.contains('kiwi 🥝')
})
Ohh much better
The loading element
One last thing we can slow down for clarity - while the network request is happening, the application is showing a loading element. But you cannot see it in the video above, can you? The network stubs are too fast to see it in the video. Thus we can add another delay just to record and be able to see the loading element.
it('returns different fruits every 30 seconds (slow down reply)', () => {
cy.clock()
// return difference responses on each call
const responses = [
['apples 🍎'], ['grapes 🍇'],
]
cy.intercept('/favorite-fruits', (req) => {
const value = responses.shift() || ['kiwi 🥝']
// wait 500ms then reply with the fruit
// simulates the server taking half a second to respond
return Cypress.Promise.delay(500, value).then(req.reply)
})
cy.visit('/fruits.html')
cy.contains('apples 🍎')
// slow down the test on purpose to be able to see
// the rendered page at each step
cy.wait(1000)
cy.tick(30000)
cy.contains('grapes 🍇')
cy.wait(1000)
cy.tick(30000)
cy.contains('kiwi 🥝')
})
If we return a Promise from the intercept, Cypress waits for the promise to resolve. Thus we can use the Bluebird's utility method Cypress.Promise.delay(ms, value)
to reply after 500ms to each request.
The video above shows each step in the application clearly, yet the test runs in a reasonable amount of time.
See more
- cy.intercept documentation page
- How cy.intercept works short video
- Presentation "How cy.intercept works" slides and recorded video
- Blog post Be Careful With Negative Assertions goes into the details on testing loading elements