-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow Dynamic Stubbing and Responses #521
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 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 I realize this'll probably be slightly outdated come 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 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: #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. |
Add flags (query parameters) to identify and stub Mesos Operator API requests to fix the integration tests that were otherwise failing or flaky due to parsing errors in the `MesosStateStore`. The store failed to parse the `GET_MASTER` response as we could only register one stub for both the `GET_MASTER` and `SUBSCRIBE` requests resulting in JSON parse errors due to the different response formats (RecoredIO<JSON>, JSON). The flags are necessary as Cypress can only stub requests based on the URL and request method and doesn't provide the means to dynamically stub requests based on the payload as documented in cypress-io/cypress#521. We will remove the flags once the Cypress team rewrote the network handling and added support for dynamic stubs as documented in cypress-io/cypress#687 or we've addressed https://jira.mesosphere.com/browse/DCOS_OSS-2021.
Add flags (query parameters) to identify and stub Mesos Operator API requests to fix the integration tests that were otherwise failing or flaky due to parsing errors in the `MesosStateStore`. The store failed to parse the `GET_MASTER` response as we could only register one stub for both the `GET_MASTER` and `SUBSCRIBE` requests resulting in JSON parse errors due to the different response formats (RecoredIO<JSON>, JSON). The flags are necessary as Cypress can only stub requests based on the URL and request method and doesn't provide the means to dynamically stub requests based on the payload as documented in cypress-io/cypress#521. We will remove the flags once the Cypress team rewrote the network handling and added support for dynamic stubs as documented in cypress-io/cypress#687 or we've addressed https://jira.mesosphere.com/browse/DCOS_OSS-2021.
Add flags (query parameters) to identify and stub Mesos Operator API requests to fix the integration tests that were otherwise failing or flaky due to parsing errors in the `MesosStateStore`. The store failed to parse the `GET_MASTER` response as we could only register one stub for both the `GET_MASTER` and `SUBSCRIBE` requests resulting in JSON parse errors due to the different response formats (RecoredIO<JSON>, JSON). The flags are necessary as Cypress can only stub requests based on the URL and request method and doesn't provide the means to dynamically stub requests based on the payload as documented in cypress-io/cypress#521. We will remove the flags once the Cypress team rewrote the network handling and added support for dynamic stubs as documented in cypress-io/cypress#687 or we've addressed https://jira.mesosphere.com/browse/DCOS_OSS-2021.
Add flags (query parameters) to identify and stub Mesos Operator API requests to fix the integration tests that were otherwise failing or flaky due to parsing errors in the `MesosStateStore`. The store failed to parse the `GET_MASTER` response as we could only register one stub for both the `GET_MASTER` and `SUBSCRIBE` requests resulting in JSON parse errors due to the different response formats (RecoredIO<JSON>, JSON). The flags are necessary as Cypress can only stub requests based on the URL and request method and doesn't provide the means to dynamically stub requests based on the payload as documented in cypress-io/cypress#521. We will remove the flags once the Cypress team rewrote the network handling and added support for dynamic stubs as documented in cypress-io/cypress#687 or we've addressed https://jira.mesosphere.com/browse/DCOS_OSS-2021.
Add flags (query parameters) to identify and stub Mesos Operator API requests to fix the integration tests that were otherwise failing or flaky due to parsing errors in the `MesosStateStore`. The store failed to parse the `GET_MASTER` response as we could only register one stub for both the `GET_MASTER` and `SUBSCRIBE` requests resulting in JSON parse errors due to the different response formats (RecoredIO<JSON>, JSON). The flags are necessary as Cypress can only stub requests based on the URL and request method and doesn't provide the means to dynamically stub requests based on the payload as documented in cypress-io/cypress#521. We will remove the flags once the Cypress team rewrote the network handling and added support for dynamic stubs as documented in cypress-io/cypress#687 or we've addressed https://jira.mesosphere.com/browse/DCOS_OSS-2021.
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": [
{
"id": 1,
"name": "Fake User 1",
"email": "fu1@fake.com"
},
{
"id": 2,
"name": "Fake User 2",
"email": "fu2@fake.com"
},
{
"id": 3,
"name": "Fake User 3",
"email": "fu3@fake.com"
}
],
"newContact": {
"id": 4,
"name": "Fake User 4",
"email": "fu4@fake.com"
}
} I return the 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
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 |
a workaround for missing dynamic routing capabilities see cypress-io/cypress#521 and cypress-io/cypress#387
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! |
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.
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
|
@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:
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. |
Bumping this, because there is still now answer if its beeing tracked. |
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. |
I think you just need to uncomment your code |
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? |
@jennifer-shehane Appreciate the update :) |
@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 cy.route({
...,
onRequest(xhr) {
xhr.setRequestHeader("X-Cypress-RESPONSE", encodeURI(fake_response))
},
response: "",
}) or use But this results in an unexpected response because the way cy.route({
onRequest(xhr) {
fake_response = "foo"
...
},
response: ""
}) the response becomes I think the least they can do is to execute cy.route({
response() {
this // What the heck is this? state("runnable")
}
}) What the heck is a runnable? And why does it have to be Or they could make 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 This is going to be very helpful for people who want to mock socket.io, like I do. You could just use 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 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 |
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 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 This comment thread has been locked. If you are still experiencing this issue after upgrading to |
The features requested in this issue are now possible as part of
Please see the If you encounter any issues or unexpected behavior while using |
Current behavior:
Currently, all mocked responses need to know the response data upfront.
The stubbed route can also include dynamic routes, but the response will always be static upon the route being initialized.
Expected behavior:
response
wasn't set, the request would continue to the server.Test code:
The text was updated successfully, but these errors were encountered: