Testing periodic network requests with cy.intercept and cy.clock combination

February 23, 2021

•

By Gleb Bahmutov

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.

GET /favorite-fruits happens every 30 seconds

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