Using "cypress-recurse" and "cy-spok" plugins to retry failing network requests
Imagine you are testing a page. The backend might take a little bit of time to respond. How do you ping the backend to know when it is ready? You want to retry cy.request network calls until it returns a success. But how would you check the response? When Murat Ozcan gets stomped by Cypress tests, he asks me:
If the network call fails or the server responds with an error, we want to retry the call after a delay. Here is how we can do it using cypress-recurse
describe('waits for API', () => { beforeEach(() => { // this call to the API resets the counter // the API endpoint /greeting will be available // in a random period between 1 and 5 seconds cy.request('POST', '/reset-api') cy.visit('/') })
it('checks API using cypress-recurse v1', () => { recurse( () => { return cy.request({ url: '/greeting', failOnStatusCode: false, }) }, (res) => { return ( res.isOkStatusCode && res.body === 'Hello!' && res.status === 200 ) }, { timeout: 6000, // check API for up to 6 seconds delay: 500, // half second pauses between retries log: false, // do not log details }, ) // now the API is ready and we can use the GUI cy.get('#get-api-response').click() cy.contains('#output', 'Hello!').should('be.visible') }) })
Sometimes validation rules are complex. This is where cy-spok shines. Let's validate the entire response object by writing predicates for each property.
describe('waits for API', () => { beforeEach(() => { // this call to the API resets the counter // the API endpoint /greeting will be available // in a random period between 1 and 5 seconds cy.request('POST', '/reset-api') cy.visit('/') })
it('checks API using cypress-recurse v2', () => { recurse( () => { return cy.request({ url: '/greeting', failOnStatusCode: false, }) }, (res) => { // use cy-spok to validate the response object // if cy-spok throws an error, // return false to retry the recursion try { spok({ body: 'Hello!', status: 200, isOkStatusCode: true, })(res) // Run spok on the response returntrue// If spok passes, return true } catch (error) { returnfalse// If spok fails, continue recursion } }, { timeout: 6000, // check API for up to 6 seconds delay: 500, // half second pauses between retries log: false, // do not log details }, ) // now the API is ready and we can use the GUI cy.get('#get-api-response').click() cy.contains('#output', 'Hello!').should('be.visible') }) })
The Command Log shows the failing cy-spok predicate body: 'Hello!'. It is nice, but the predicate checking became more verbose, I don't like it
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
(res) => { // use cy-spok to validate the response object // if cy-spok throws an error, // return false to retry the recursion try { spok({ body: 'Hello!', status: 200, isOkStatusCode: true, })(res) // Run spok on the response returntrue// If spok passes, return true } catch (error) { returnfalse// If spok fails, continue recursion } },
We are running spok(predicates)(res) just to catch an error, then return true/false. Luckily cypress-recurse supports thrown exception directly in its synchronous predicate callback. We could simply write
describe('waits for API', () => { beforeEach(() => { // this call to the API resets the counter // the API endpoint /greeting will be available // in a random period between 1 and 5 seconds cy.request('POST', '/reset-api') cy.visit('/') })
it('checks API using cypress-recurse v3', () => { recurse( () => { return cy.request({ url: '/greeting', failOnStatusCode: false, }) }, // spok returns a function that will be called with the response // and will throw if the response does not pass the predicates spok({ body: 'Hello!', status: 200, isOkStatusCode: true, }), { timeout: 6000, // check API for up to 6 seconds delay: 500, // half second pauses between retries log: false, // do not log details }, ) // now the API is ready and we can use the GUI cy.get('#get-api-response').click() cy.contains('#output', 'Hello!').should('be.visible') }) })