Cypress: Allow Dynamic Stubbing and Responses

Created on 1 Jun 2017  ·  41Comments  ·  Source: cypress-io/cypress

Current behavior:

Currently, all mocked responses need to know the response data upfront.

cy
  .server()
  .route("/users", [{id: 1, name: "Kyle"}])

The stubbed route can also include dynamic routes, but the response will always be static upon the route being initialized.

cy
  .server()
  .route(/api\.server\.com, SingleFixture)

Expected behavior:

1) The ability to setup fixture responses dynamically. Perhaps using some sort of controller function.

cy
  .server()
  .route(/api\.server\.com/, FixtureController)

function FixtureController(request, response) {
  if (request.url.contains('users') {
    response.body = [{id: 1, name: "Pat"}]
    return request
  }

  if (request.url.contains('login') {
    response.body = { token: '123abc' }
    return request
}

2) The ability to match all routes, but conditionally allow some responses to be stubbed, and others to request real data. In the above, perhaps if response wasn't set, the request would continue to the server.

Test code:

function FixtureController(request, response) {
    if (request.url === 'api.server.com/users') {
        response.body = { hello: "world" }
        return response
    }
}
describe(`Dynamic Stubbing`, () => {

    beforeEach(() => {
        cy.server().route(/api\.server\.com/, FixtureController)
    })

    it('should stub a route dynamically', () => {
        cy.request('http://api.server.com/users').then(function(response) {
            expect(response.body).to.have.property("hello", "world")
        })
    })
})

  • Operating System: OSX
  • Cypress Version: 0.19.2
  • Browser/Browser Version: Chrome Latest
network feature

Most helpful comment

So we are almost 2 years after this problem was initially brought up. I see people have written some pseudo fixes for different problems, but none of them seem like a catch all (at least for XHR).

Does anyone from Cypress have an update on this? @brian-mann ?

Love Cypress, and I'd like to be able to use it more dynamically.

All 41 comments

I second this 👍

I found myself wanting it for POST requests mostly. I think you could make much more complicated and realistic integration tests if it was possible to return whatever the request got and tack on an id or createdAt.

cy
  .server()
  .route('POST', /api/comment, (xhr) => {
     const comment = xhr.request.body
     comment.id = 1
     return comment
  })

Also great work so far, I love the possibilities it's opening up!

I think this is possible now using route's response method?

I realize this'll probably be slightly outdated come 0.20.0 but we solve this issue like this:

Cypress.addParentCommand({
    routeUsers(mutator = _.identity) {
        cy.fixture('users').as('fxUsers');

        cy.route({
            url: '/users/*',
            response() {
                return mutator({
                    data: this.fxUsers
                });
            }
        }).as('routeUsers');
    }
});

And later we do something like:

// Only return 2
cy.routeUsers(fx => {
  return _.sample(fx.users, 2);
});
cy.visit('some-foo.html#users').wait('@routeUsers');

@paulfalgout That doesn't really solve the problem though because you still don't have access to the request object.

The response callback should be passed the request object. I also like @tracykm 's suggestion of making the response argument be able to be a function and not just a String or Object. I can submit a PR if that API can be agreed upon.

@ianwalter we are not likely going to change or update any of the existing route / wait API's because ultimately the whole thing needs to be rewritten and implemented in a completely different fashion.

Here's an issue describing it: https://github.com/cypress-io/cypress/issues/687

When we rewrite those API's we will take this use case into account.

@brian-mann any chance that we can enable dynamic stubs on top of the current API today? The epic you were referring to looks like a rather big undertaking and the lack of dynamic stubs is currently blocking us.

Unable to write certain test cases because of this issue, it's quite depressing :(

I'm new to Cypress and pretty quickly ran into the situation where I would've liked to be able to return a dynamic response based on the request.body, which led me here. After kind of grumbling that I couldn't build up a response object based off of the request, I eventually landed what I think is an acceptable solution for @tracykm 's use case. It's not dynamic but I find that it more aligns with Cypress' mantra of not having flakey tests.

My specific case was that I wanted to test that a user could add a new contact. I ended up splitting my contacts fixture up to be the list contacts that'd be initially loaded, called existingContacts, and a single contact to be added, named newContact:

{
  "existingContacts": [
    {
      "id": 1,
      "name": "Fake User 1",
      "email": "[email protected]"
    },
    {
      "id": 2,
      "name": "Fake User 2",
      "email": "[email protected]"
    },
    {
      "id": 3,
      "name": "Fake User 3",
      "email": "[email protected]"
    }
  ],
  "newContact": {
      "id": 4,
      "name": "Fake User 4",
      "email": "[email protected]"
    }
}

I return the existingContacts in the GET version of the /contacts route. The newContact is first aliased so that I can reference it later on in the test and then returned in the POST version of /contacts. Finally, I can use this.newContact.<prop> as the input values during my test (notice I'm using a regular anonymous function for the test and not a lambda/fat-arrow function).

describe('Users', function() {
  beforeEach(function() {
    cy.fixture('contacts').then(({ 
      existingContacts,
      newContact,
    }) => {
      cy.wrap(newContact).as('newContact');

      cy.server()
        .route({
          method: 'GET',
          url: Cypress.env('SERVER_URL') + '/contacts',
          response: existingContacts,
        }).as('getContacts')
        .route({
          method: 'POST',
          url: Cypress.env('SERVER_URL') + '/contacts',
          response: newContact,
        }).as('postContact');
    });
  });

  it('can add a new contact', function() {
    cy.visit('/', {
      // https://github.com/cypress-io/cypress/issues/95#issuecomment-281273126
      onBeforeLoad: (win) => {
        win.fetch = null;
      }
    })
      .wait('@getContacts')
      .get('.add-contact')
      .click()
      .get('input[name="name"]')
      .type(this.newContact.name)
      .get('input[name="email"]')
      .type(this.newContact.email)
      .get('button')
      .click()
      .wait('@postContact')
      .get('.contact-list > li')
      .should('have.length', 4);
  });
})

I feel more confident testing this way as it takes any code I would've written to pass the request body through to my response out of the equation i.e. server logic, the whole point of stubbing routes. Anyway, hopefully this is coherent and I thought I'd add to the thread seeing as it's not closed yet and I ran into this issue almost immediately.

I'd like to add, that it's important not only to support dynamic response body, but the status, too. I'm trying to mock the resumable upload into google storage and while redefining the same route inside a single test works in windowed mode, it doesn't work in the headless mode - I guess the application executes requests quicker than cypress is capable of changing the route stub. The problem is a situation where a single PUT endpoint works in two modes:

  1. To check whether the upload was already started before and it's about to be resumed or started from scratch.
  2. The upload itself.

I want to test upload retries in our app, so I need 1. to pass (ideally talk to the live server) and stub 2. to return a 404 and not only I don't know how to do it, I don't even know if it's possible with cypress.

My code works with PayPal's Payflow API and multiple requests to the same endpoint return different values depending on the request. I must have either a reference to the request or the ability to return different responses for each subsequent request.

Similar problem with testing on app using GraphQL. Expecting a dynamic response dependant on request body. Very hard/hacky to test with cypress at the moment. Current solution to use callFake() on the stubbed 'fetch' -> graphqlURL to allow us to introduce logic dependant on request body.

const responseStub = result => ({
  json() {
    return Promise.resolve(result);
  },
  text() {
    return Promise.resolve(JSON.stringify(result));
  },
  ok: true,
});

Cypress.Commands.add('visitStubbed', (url, operations = {}) => {
  cy.visit(url, {
    onBeforeLoad: win => {
      cy.stub(win, 'fetch')
        .withArgs('/graphql')
        .callsFake((_, req) => {
          const { operationName } = JSON.parse(req.body);
          const resultStub = get(operations, operationName, {});
          return Promise.resolve(responseStub(resultStub));
        });
    },
  });
});

The only problem is that this hides the request within the cypress promise itself and thus never exposed to the test. We are having to assume it's correct from the basic response logic return. Not ideal and therefore would be awesome if the network request stubbing would allow us to use the request to apply logic in forming the response. We could then spy on the fetch/XHR call to graphql to expose the request itself to the tests for full e2e integration testing.

@henryhobhouse I have enhanced my fetch stubbing abilities to allow for dynamic responses and conditional stubbing (i.e. letting real request go through sometimes) can share the code if you'd like

@egucciar Sounds great!

Me too please.

Sent from my iPhone

On Oct 5, 2018, at 6:48 AM, Henry Hobhouse notifications@github.com wrote:

@egucciar Sounds great!


You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub, or mute the thread.

Sorry guys for the delay i totally forgot. BE AWARE this is only for fetch even though the subject matter of the post is XHR requests, this is in response to henryhobhouse's wish to be able to stub out his graphQL requests.

Similar problem with testing on app using GraphQL.

So this is 100% only geared towards fetch/graphQL & I hope that it helps. Sorry it probably wont be great for the XHR usecase

export const stubFetch = (stubs = defaultStubs) => win => {
  const { fetch } = win;
  cy.stub(win, 'fetch', (...args) => {
    console.log('Handling fetch stub', args);
    const [url, request] = args;
    if (!request) {
      return fetch(...args);
    }
    const postBody = JSON.parse(request.body);
    let promise;
    if (url.indexOf('graphql') !== -1) {
      stubs.some(stub => {
        if (postBody.operationName === stub.operation) {
          console.log('STUBBING', stub.operation);
          const response = makeApolloResponse(stub.response, postBody);
          // if response is false, let the request go through anyway
          // i.e. conditionally stub this operation if the response is not false
          if (response === false) {
            return false;
          }
          promise = Promise.resolve(response);
          return true;
        }
        return false;
      });
    }
    if (promise) {
      return promise;
    }
    console.warn('Real Fetch Getting Called With Args', args);
    return fetch(...args);
  });
};

function makeApolloResponse(resp, body) {
  const response = isFunction(resp) ? resp(body) : resp;
  if (response === false) {
    return false;
  }
  return {
    ok: true,
    json() {
      return Promise.resolve(response);
    },
    text() {
      return Promise.resolve(JSON.stringify(response));
    },
    clone() {
      return {
        arrayBuffer() {
          return {
            then() {
              return {
                byteLength: 10,
              };
            },
          };
        },
      };
    },
  };
}

function isFunction(x) {
  return Object.prototype.toString.call(x) === '[object Function]';
}

This is how stubs can be defined
```
cy.visit('/', {
onBeforeLoad: stubFetch([
{
operation: 'gqlNamedOperation',
response: { // json response - static }
},
{
operation: 'gqlNamedOperation2',
response: (body) => {
if ( // some condition) {
return // json response
}
return false; // lets real request pass through / real response is returned
},
])
}
)

@egucciar Nice!

Here is an alternative workaround I wrote a while ago using xhook to intercept XHRs:

  cy.visit('/checkout/review', {
    onBeforeLoad: window => {
      const script = window.document.createElement('script')
      script.onload = function () {
        window.xhook.before(req => {
          if (req.method === 'post' && req.url === '/api/orders') {
            const body = JSON.parse(req.body)
            body.stripeToken = 'tok_visa'
            req.body = JSON.stringify(body)
          }
        })
      }
      script.src = '//unpkg.com/xhook@latest/dist/xhook.min.js'
      window.document.head.appendChild(script)
    }
  })

Just a short idea about dynamic changing of responses:

describe('Change Response', () => {
  const comments = [
    { id: 1, text: 'First comment' },
  ];

  beforeEach(() => {
    cy.server();
    cy.route('GET', '/comments', comments);
    cy.visit('/')
  });

  it('First call with one comment', () => {
    cy.get('[data-cy=comment]').should('have.length', 1);
  });

  it('First call with one comment', () => {
    comments.push({
      id: 2, text: 'Second comment',
    });
    cy.get('[data-cy=comment]').should('have.length', 2);
  });
});

Of course this idea has a problem: you will not have a proper value while using time travel (but i can live with this problem for now and just wait for the bright future).

I found a solution for the following scenario:

  1. Get list of all speakers
  2. Update one of them
  3. Request again all speakers (which expects one of them to be updated)
beforeEach(() => {
    cy.server();
    cy.route({
      method: 'GET',
      url: '**/api/v1/speaker',
      response: 'fixture:ResponseSpeakerMulti.json',
      delay: 500,
    }).as('getAllSpeakers');

    cy.route({
      method: 'PUT',
      url: '**/api/v1/speaker/2',
      response: 'fixture:ResponseSpeakerSingleUpdate.json',
      delay: 500,
    }).as('updateSpeaker');
});

it('should update after edit', () => {
    cy.wait([ '@getAllSpeakers' ]);

    // Overwrite the route.
    cy.route({
      method: 'GET',
      url: '**/api/v1/speaker',
      response: 'fixture:ResponseSpeakerMultiUpdated.json',
      delay: 500,
    }).as('getAllSpeakersUpdated');

    cy.get('#edit-2').click();
    cy.get('input[formcontrolname="lastName"]').clear().type('Doe');
    cy.get('[type="submit"]').click();
    cy.wait([ '@updateSpeaker' ]);
    cy.wait([ '@getAllSpeakersUpdated' ])
});

So we are almost 2 years after this problem was initially brought up. I see people have written some pseudo fixes for different problems, but none of them seem like a catch all (at least for XHR).

Does anyone from Cypress have an update on this? @brian-mann ?

Love Cypress, and I'd like to be able to use it more dynamically.

@alexlee-dev Hey, just an update, this is some work that will be done as part of #687, which is the next feature I'm working on. It will allow you to dynamically modify any type of network request in-flight in Cypress.

For those still looking for a stubbing workaround based on the GraphQL query that's typically sent with the request body, here's a variation based of @ianwalter's suggestion to use xhook to intercept the creation of XHR requests:

/cypress/support/index.ts

let xHookPackage;
let graphqlEndpoint='XXXXXXX';
let mockGraphQLResponsesMap = {
    queryA: {} // some mock response
    queryB: {} // another mock response
}

before(() => {
    const xHookUrl = 'https://unpkg.com/xhook@latest/dist/xhook.min.js';
    cy.request(xHookUrl)
        .then(response => {
            xHookPackage = response.body;
        });
});

Cypress.on('window:before:load', win => {
    // load the library in the cypress window, creates a 'xhook' object on the Window
    win.eval(xHookPackage);
    // tap into the .before() method 
    win.xhook.before(req => {

        if (req.method === 'POST' && req.url === graphqlEndpoint) {
            const graphqlQuery = JSON.parse(req.body).query;
            // example:  "query GetAuthUser($cognito_id: String!) { getAuthUser(....", we want the first part
            const graphqlQueryString = graphqlQuery.split('(')[0];
            const graphQLOperationType = graphqlQueryString.split(' ')[0];
            const graphQLOperationName = graphqlQueryString.split(' ')[1];
            console.warn(
                `Stubbing graphQLOperationType ${graphQLOperationType} and graphQLOperationName ${graphQLOperationName}`
            );

            const mockResponseObject = mockGraphQLResponsesMap[graphQLOperationName]; // no need to stringify
            return {
                status: 200,
                text: mockResponseObject,
            };
        }
    });
});

Hi all,
any news about this issue? @flotwig
I'm actually struggling to create a response based on a POST request.
I think this feature is really really useful!!

Hello,
Just discovered cypress two days ago, and immediately run into test case where I want to check request body (basically, write an assertion in route callback function checking if app sends valid requests upon user actions). And then I discovered this thread :\

some simple workaround:
so i started writing some tests today with cypress, and pretty fast got to a point where they have yet not made a solution with their API.
the case was:
in the dashboard, each widget request a query and expects a response. since widgets have lots of configuration that affects that response there aren't (or nearly none) any 2 responses that are identical, and the response is needed to correctly render the widgets.
so the use case is as follows:
widget#1 request a query with campaigns (by name).
widget#2 requests a query with CTR(metric) Media buy key(dim).
i needed to know what widget triggered the request, and set the response by a param that's delivered in the payload.
sounds straight forward but Cypress has an open issue where many developers required this functionallity.
so considering the following API for stubbing requests:

cy.route({
  method: 'POST',
  url: '**/query',
  response: ''
 onResponse: (rawResponse) => {
    // do something with the
    // raw XHR object when the
    // response comes back
  }
})

notice that the onResponse method is only triggered when the response is already sent.
so what i needed to do is changing the response by reference in the onResponse method.
something like

 onResponse: (rawResponse) => {
if(myCondition === rawResponse.request.body.myParam) {
     xhr.response = {  A: 'OFIR' }
} 
}

tried to do it, but still saw that
xhr.response = ''
contained nothing.
so i looked further, and checked what causes it not to use the value i set to it, and found out that the response property isn't writable.
how ?

Object.getOwnPropertyDescriptor(xhr, 'response');
// {value: {...}
// writable: false
// enumerable: true
// configurable: true}

ok good, so we need to set it as writable. piece of :cake:
Object.defineProperty(xhr.__proto__, 'response', { writable: true });
now my on response method looks like that:
onResponse: (rawResponse) => {

if(myCondition === rawResponse.request.body.myParam) {
     xhr.response = { A: 'OFIR' } //now xhr.response realy is what i wanted
}
}

created a method to set it writable for each response:

// send the onResponse argument.
function getResponseWritable(rawResponse) {
  const { xhr } = rawResponse;
  Object.defineProperty(xhr.__proto__, 'response', { writable: true });
  return rawResponse;
}

Any updates? Any working solution?

Bumping this, because there is still now answer if its beeing tracked.
In my opinion its something essential.

Any update on this at all? Updating values of the real response would be an amazing capability for us in testing many scenarios with real data without having to stub the whole thing. @flotwig :)

Any update on this? It doesn't work even if I changed/mutated the real response in onResponse method similar like:

cy.route({
        url: 'xxx/url',
        onResponse: (xhr) => {
          xhr.response.body.xxx=xxx;
          // or even return the response. 
         // return { ...xhr.response }
        },
      });

I still got the real response rather than the updated one.

Any update on this? It doesn't work even if I changed/mutated the real response in onResponse method similar like:

cy.route({
        url: 'xxx/url',
        onResponse: (xhr) => {
          xhr.response.body.xxx=xxx;
          // or even return the response. 
         // return { ...xhr.response }
        },
      });

I still got the real response rather than the updated one.

I think you just need to uncomment your code return { ...xhr.response }. Seems to work for me.

Looking at the documentation, onResponse is a callback function and doesn't accept any returned data.

@sam3k Did you test mutating the data? I've tried locally in my tests to change the data and return onResponse and doesn't seem to work. Would love to know if this is possible changing something?

This feature will be a part of the work delivered in #4176 for #687.

I realize it seems like it has not had work in a while. We are working on first delivering #5273 which is a dependency of continuing with the Network Layer rewrite.

@jennifer-shehane Appreciate the update :)

Any update on this? It doesn't work even if I changed/mutated the real response in onResponse method similar like:

cy.route({
        url: 'xxx/url',
        onResponse: (xhr) => {
          xhr.response.body.xxx=xxx;
          // or even return the response. 
         // return { ...xhr.response }
        },
      });

I still got the real response rather than the updated one.

@hehoo I have the same behavior

Did you find out a solution?

This feature will be a part of the work delivered in #4176 for #687.

I realize it seems like it has not had work in a while. We are working on first delivering #5273 which is a dependency of continuing with the Network Layer rewrite.

Do you have any time estimation for delivery?

Any update on this? It doesn't work even if I changed/mutated the real response in onResponse method similar like:

cy.route({
        url: 'xxx/url',
        onResponse: (xhr) => {
          xhr.response.body.xxx=xxx;
          // or even return the response. 
         // return { ...xhr.response }
        },
      });

I still got the real response rather than the updated one.

@hehoo I have the same behavior

Did you find out a solution?

No, I mock the request to solve my issue. Waiting for the fix in cypress :)

Apparently, Cypress uses X-Cypress-RESPONSE header to stub the response. And one way to do this is to use:

cy.route({
  ...,
  onRequest(xhr) {
    xhr.setRequestHeader("X-Cypress-RESPONSE", encodeURI(fake_response))
  },
  response: "",
})

or use encodeURI(JSON.stringify(fake_response)) if the fake_response is an object value as done in this line of the code.

But this results in an unexpected response because the way setRequestHeader works. It adds the fake_response after ,. So if you had:

cy.route({
  onRequest(xhr) {
    fake_response = "foo"
    ...
  },
  response: ""
})

the response becomes , foo. With JSON type, the type becomes a string. If you have no problem with working with such an awkward response, you are good to go.

I think the least they can do is to execute o.response done in this code later and do it in here where they add all the headers. What's the context of:

cy.route({
  response() {
    this // What the heck is this? state("runnable")
  }
})

What the heck is a runnable? And why does it have to be this of response instead of being a parameter?

Or they could make shouldApplyStub (code):

    shouldApplyStub (route) {
      return hasEnabledStubs && route && (route.response != null)
    },

more strict or lenient and do

... && (route.response === null` or `route.response != null || route.stubUsingHeader == true)

and let us fake the response using setRequstHeader with encodeURI(JSON.stringify(...)). I think this is better for us and them for the time being if it's too difficult to check if response is a function and call it within applyStubProperties.


This is going to be very helpful for people who want to mock socket.io, like I do. You could just use cy.route({ url: "/socket.io/*" }) method GET to get response and POST to get requests. And I don't want to care about websocket (or socket.io) handshake protocol.

So, by giving me the access to the request object, I can decide whether to use the fake response or not; don't fake it if it's a handshake protocol

So much potential in a 3 year and 10 days old issue. Please!!

@chulman444 Thanks for inspiration!

My workaround works on v4.9.0:

  cy.route({
    method: 'POST',
    url: `/searches`,
    response: '{"errors": {}',                             // First part of response (1)
    onRequest: ({ xhr, request }) => {
      const { sessionId } = request.body.data[0];          // Data from request POST body
      xhr.setRequestHeader(
        'X-Cypress-Response',
        `"data": { "${sessionId}": "9A2183464FA47403" }}`, // Second part of response (2)
      );
    },
  }).as('searches');

Cypress's will join response (1) key and X-Cypress-Response (2) with a comma, when returns response for cy.route.

Thanks @ianwalter @cy6581! This was a big blocker for us and your solutions work well. I decided to put xhook into a local fixture though as I wasn't keen to rely on the CDN.

@cbovis I wasnt able to get their solution to work. Are you able to share a code snippet for an example?

Sure thing @cain.

Place the contents of https://unpkg.com/xhook@latest/dist/xhook.min.js into cypress/fixtures/xhook-js.txt (or wherever your fixtures folder is). Make sure you've got fixtures enabled in cypress config.

Reusable helper (can be reworked to fit your needs):

/**
 * Command which allows us to conditionally mock XHR calls. This is actually
 * quite difficult to do out of the box and there's a three year old issue
 * tackling the problem still open on GitHub:
 *
 * https://github.com/cypress-io/cypress/issues/521
 *
 * This solution is constructed from some of the suggestions there.
 */
Cypress.Commands.add('mockXhrCalls', mocks => {
  cy.fixture('xhook-js').then(xHookJs => {
    Cypress.on('window:before:load', win => {
      /**
       * Load xhook JS into the headless browser environment.
       * After loading it will be available via window.xhook
       */
      win.eval(xHookJs);

      mocks.forEach(mock => {
        win.xhook.before(req => {
          if (req.method === mock.method && req.url.endsWith(mock.urlPath)) {
            if (_.isFunction(mock.response)) {
              return mock.response(req);
            }

            return mock.response;
          }

          return undefined; // Pass request through to actual API
        });
      });
    });
  });
});

Example of consumption inside test suite:

cy.mockXhrCalls([
      {
        method: 'POST',
        urlPath: '/gql',
        response: request => {
          const body = JSON.parse(request.body);
          const { query } = body;

          if (query.indexOf('updateProfile') !== -1) {
            const mockResponse = {
              data: {
                success: true,
              },
            };

            return {
              headers: {
                'content-type': 'application/json; charset=utf-8',
              },
              status: 200,
              text: JSON.stringify(mockResponse),
              data: mockResponse,
            };
          }

          // Anything which didn't match the above condition should pass-through to original API
          return undefined;
        },
      },
    ]);
  });

Released in 5.1.0.

This comment thread has been locked. If you are still experiencing this issue after upgrading to
Cypress v5.1.0, please open a new issue.

The features requested in this issue are now possible as part of cy.route2().

cy.route2() is currently experimental and requires being enabled by passing "experimentalNetworkStubbing": true through your Cypress configuration. This will eventually be merged in as part of our standard API.

Please see the cy.route2() docs for full details: https://on.cypress.io/route2

If you encounter any issues or unexpected behavior while using cy.route2() we encourage you to open a new issue so that we can work out all the issues before public release. Thanks!

Was this page helpful?
0 / 5 - 0 ratings