DEV Community

loading...

How to use stub multiple API requests dynamically in Cypress

digitaledawn profile image Aurore Trunelle Updated on ・2 min read

Update: You can now to this with Cypress new intercept command 🎉: https://www.cypress.io/blog/2020/11/24/introducing-cy-intercept-next-generation-network-stubbing-in-cypress-6-0/


Recently, as part of refactoring on how we handle mocking Cypress network request, I had to find a way to mock the same request made multiple times but with different responses in our test suite.

At the moment, with Cypress, there is no way you can make the mock results dynamic depending on what was requested.

The problem

We have a POST request made to a very simple endpoint. Depending on the request, we get different results.

We already have a very long file with non-formatted JSON responses that are linked to the request using a matching object to match the request payload to a specific answer dynamically. So we already have some of the logic here, which will take ages to extract or break down. Ideally, we want to reuse this.

Some of the cypress default commands were overwritten ( routes and visit) to handle this case, as well as mocking fetch.

We moved away from this and removed those to use the default cypress commands. We are using the trick describe here to mock fetch. Now we need to handle the dynamic stubbing part as well.

The solution

This is inspired from a comment on this Cypress issue on GitHub related allowing dynamic stubbing.

We have added a new command to mock request to our endpoint dynamically on-demand using xhook (library to intercept and modify XHR request and responses) :

// commands.js 
    Cypress.Commands.add('mockArticlesRequest', () => {
      Cypress.once('window:before:load', window => {
        const script = window.document.createElement('script');
        script.onload = function() {
          window.xhook.after((request, response) => {
            const { method, url, body } = request;
            if (
              method === 'POST' &&
              (url.endsWith('/articles'))
            ) {
              const articlesResponses = require('../fixtures/responses.json');
              const parsedBody = JSON.parse(body);
              const newResponse = articlesResponses.find(({ matches }) =>
                Object.keys(matches).every(
                  key =>
                    JSON.stringify(parsedBody[key]) === JSON.stringify(matches[key])
                )
              );
              if (!newResponse) {
                return;
              }
              response.data = JSON.stringify(newResponse.response);
              response.text = JSON.stringify(newResponse.response);
            }
          });
        };
        script.src = '//unpkg.com/xhook@latest/dist/xhook.min.js';
        script.id = 'xhook';
        window.document.head.appendChild(script);
      });
    });
Enter fullscreen mode Exit fullscreen mode

I'm using Cypress.once so xhook script is not added to other tests on page load.

Then in a test file where calls to that endpoint will be made, we use

// articles.spec.js
describe('Articles page', () => {
  beforeEach(() => {
    cy.mockArticlesRequest();
    cy.server();
    cy.route('/user', 'fixture:user.json');
     ...
  });
});
Enter fullscreen mode Exit fullscreen mode

And this what the fixture response file look like:

// fixtures/responses.json
{
  "matches": {
    "id": 1
  },
  "response": {
    "data": [{
      ...
   }]
  }
}
Enter fullscreen mode Exit fullscreen mode

And that's it 🎉 Happy testing 🚀

Note: There seems to an issue where this doesn't work when cy.clock is used.

Discussion (6)

Collapse
dxcjbeck profile image
Jonathan Matthew Beck • Edited

You can replace the command.js with this?

const apolloResponses = require('../fixtures/responses.json');
beforeEach(() => {
cy.server({ method: 'POST' })
cy.route('**/articles', apolloResponses )
})

Collapse
digitaledawn profile image
Aurore Trunelle Author • Edited

I don't think this fit what we needed to achieve. With this,apolloResponses is going to be returned as the response for all request whether we want apolloResponses to have multiple responses and be matched against the request payload parameters dynamically.

(apolloResponses was meant to be articlesResponses in this example 🤦‍♀️, I updated that.)

Collapse
dxcjbeck profile image
Jonathan Matthew Beck • Edited

What I meant to say is can you do away with the external dependency of xhook and use the cypress api instead:

beforeEach(() => {
cy.server({ method: 'POST' })
cy.route({
method: 'POST',
url: '**/articles',
onResponse: (xhr) => {
// insert your JSON filtering code here
}
})
})

docs.cypress.io/api/commands/route...

Thread Thread
digitaledawn profile image
Aurore Trunelle Author • Edited

That was the first thing I tried but that didn't work for this use case. I wish it did.

Collapse
marcromotarre profile image
marcromotarre

Hi, a lot of times its not calling my Cypress.Commands.add("mockArticlesRequest"

is it happening to you? sometimes pass my tests correctly, sometime not.

Thanks

Collapse
digitaledawn profile image
Aurore Trunelle Author

Hi. No, it's been working perfectly. When are you calling the command? What error do you get when it's failing?

Forem Open with the Forem app