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
A Solution for Stubbing GraphQL Requests #122
Comments
This is excellent. We'll turn this into a bona-fide recipe. This would also be worth extracting to an npm module which could be distributed as a plugin / extension for Cypress. Once we finish cypress-io/cypress#684 it'll make this a lot easier. You could then generate the mock GraphQL server during a hook inside Cypress itself (as its initializing) and then bootstrap Cypress with this data (or read it in later, etc) |
This is also a great example of how you can write e2e tests without a server but still have nearly 100% test coverage for the backend. You would be retaining all the guarantees that the contract is correct between the client and server but not have to spin one up or wait for slow network calls. |
awesome! Yeah, GraphQL is pretty cool that way, this falls right into place when using 'schema driven development'! The plugin/hook would be great for this. My ideal goal is to be able to run the tests against an actual graphql server and the stubs with only environment configuration needed, but for that I'd want to be able to alias and wait on the requests so maybe there will be a way to shoehorn this into the normal |
This solution worked for our team, but with some slight alterations. I thought it would be useful to share that here. The calls to cy.visit(url, {
onBeforeLoad: (win) => {
cy.stub(win, 'fetch').callsFake(serverStub).as('fetch stub');
},
}); (As you can see, we also labeled the stub. This makes it easy to spot inside the Cypress test screen.) This also made the sub match all fetch requests, so inside serverStub() we had to make sure those were matched. For this, the first parameter if (path === '/our-custom-non-graphql-path') {
return Promise.resolve(responseStub({ foo: 'bar' }));
} In addition, the function responseStub(result) {
return {
json() {
return Promise.resolve(result);
},
text() {
return Promise.resolve(JSON.stringify(result));
},
ok: true,
};
} We don't use the return Promise.reject(new Error(`Not found: ${path}`)); With these alterations, we are now able to mock multiple different GraphQL requests. Thanks for the tip! |
Thanks so much for these solutions so far! I used a lot of the code in this discussion to create an entirely separate const responseStub = result => Promise.resolve({
json() {
return Promise.resolve(result);
},
text() {
return Promise.resolve(JSON.stringify(result));
},
ok: true,
});
Cypress.Commands.add('mockGraphQL', (handler) => {
cy.on('window:before:load', (win) => {
const originalFunction = win.fetch;
function fetch(path, { body, method }) {
if (path.includes('/graphql') && method === 'POST') {
return responseStub(handler(JSON.parse(body)));
}
return originalFunction.apply(this, arguments);
}
cy.stub(win, 'fetch', fetch).as('graphqlStub');
});
}); With that, I can then simply call cy.mockGraphQL(({ operationName, variables }) => {
return { data: getAppropriateData(operationName, variables) };
}); Note that I used the event approach described here rather than overriding visit. I think this approach is a little tighter and more explicit than the other solutions so far, but that may just be my personal preference. Sharing here in case someone else feels the same way. |
and here is my solution inspired by all of yours thanks ;-) const responseStub = result => Promise.resolve({
json: () => Promise.resolve(result),
text: () => Promise.resolve(JSON.stringify(result)),
ok: true,
})
Cypress.Commands.add('visitWithGraphQLStub', (url, options) => {
cy.fixture('graphql').then((mockedData) => {
function fetch(path, { body }) {
const { operationName } = JSON.parse(body)
return responseStub(mockedData[operationName])
}
cy.visit(url, Object.assign({
onBeforeLoad(win) {
cy.stub(win, 'fetch')
.withArgs(API_URL)
.as('fetchGraphQL')
.callsFake(fetch)
},
}, options))
})
})
{
"queryName": {
"data": {
"queryCalled": {
// data
}
}
}
} |
Maybe I'm doing something wrong, but using
So if you replace Fortunately all we had to do instead was just |
Actually if you return a promise from your stub that should work fine. Stubbing fetch worked great for us but we're worried that the structure of graph ql may change and our stubs will break in the future. |
Hmm, I definitely must be doing something wrong then, because I keep getting |
Paste your code? |
I mean, I literally was just using the example from above: onBeforeLoad: win => {
cy
// stub `fetch`
.stub(win, 'fetch')
// your graphql endpoint
.withArgs('/graphql')
// call our stub
.callsFake(serverStub)
}, And then returning promises from |
If you paste the code for server stub then maybe I can help you, but for all I know you could be missing a return statement in your server stub or something silly. It works. |
@bmakuh did you solve it? |
@egucciar using this code: const responseStub = result =>
Promise.resolve({
json: () => Promise.resolve(result),
text: () => Promise.resolve(JSON.stringify(result)),
ok: true,
})
Cypress.Commands.add('visitWithGraphQLStub', (url, options) => {
cy.fixture('graphql').then(mockedData => {
function fetch(path, { body }) {
const { operationName } = JSON.parse(body)[0]
return responseStub(mockedData[operationName])
}
cy.visit(
url,
Object.assign(
{
onBeforeLoad(win) {
cy
.stub(win, 'fetch')
.withArgs(graphQlEndpoint)
.as('fetchGraphQL')
.callsFake(fetch)
},
},
options
)
)
})
}) and then called in a spec by cy.visitWithGraphQLStub(host) results in the by @bmakuh mentioned error |
Worth mentioning, this is on Chrome 66.0.3359.170 on MacOS, updated today. I checked out our code to a revision from last week where this issue did NOT exist, and it DOES exist now. Could it be related to chrome version? Check this out in Sinons repo sinonjs/sinon#1341 (comment) |
So according to sinonjs/sinon#1341 (comment), this behaviour is intended in Sinon 2.. , meaning that callsFake will by design not work with "thennable" stubs (unless there is a way and I'm just missing it) Solution I implemented due to this, which is a slight modification to @canercandan and works Cypress.Commands.add('visitWithGraphQLStub', (url, options) => {
cy.fixture('graphql').then(mockedData => {
function mockFetch(path, options) {
if (path == graphQLEndpoint) {
const { operationName } = JSON.parse(options.body)[0]
const response = responseStub(mockedData[operationName])
return response
}
return fetch(path, options)
}
cy.visit(
url,
Object.assign(
{
onBeforeLoad(win) {
win.fetch = mockFetch
},
},
options,
),
)
})
}) |
I appreciate the amount of work that's gone into this thread, but can someone summarize it into an implementation and an API example so that someone who doesn't understand how all this stuff works can get it to work? |
Can anyone please provide minimal working solution, its kind of hard understanding this without in detail understanding how to setup cypress. |
I can paste a sample I got working shortly. |
Here is an example that works for me. Please ask if any Qs. Do not just copy-paste and expect to work as is, please read the comments and adjust to suit your needs. /**
* this function requires `window` to be passed
* example 1: cy.visit('/', {
* onBeforeLoad: stubFetch
* })
*
* example 2: cy.window().then(stubFetch);
*
* also requires an array of `stubs` to be present in outer scope
*
* example:
const stubs = [
{
operation: 'QueryOperationName',
response: // JSON response to send
},
* this is so we can handle different kinds of operations
* and stub them with the appropriate responses
*/
export function stubFetch(win) {
const { fetch } = win;
cy.stub(win, 'fetch', (...args) => {
console.log('Handling fetch stub', args);
const [url, request] = 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);
promise = Promise.resolve({
ok: true,
text() {
return Promise.resolve(JSON.stringify(stub.response));
},
});
return true;
}
return false;
});
}
if (promise) {
return promise;
}
console.warn('Real Fetch Getting Called With Args', args);
return fetch(...args);
});
} |
@egucciar Thanks for taking your time for posting this. Later after my posting I figure it out. I had trouble with not knowing where to include the changes, and how to use them. I also decide to use server mocking as my particular scenario don't work well with stubing. |
I really liked the above function, so i changed it to accept in stubs, instead of having global stubs. This allows each spec to only pass in the specs it wants, as we're either testing successful or failure for mutations sometimes (though this allows both queries AND mutations, obviously). One thing to note is that I changed the function so if it doesn't find a matching operation in the stub, it'll fail. This is so we don't accidentally send through requests to our API (though we never should, it's just a precaution :)). export const graphqlFetch = (win, stubs) => {
const { fetch } = win;
cy.stub(win, "fetch", (...args) => {
console.log("Handling fetch stub", args);
const [url, request] = args;
const postBody = JSON.parse(request.body);
let promise;
if (url.indexOf("api") !== -1) {
stubs.some(stub => {
if (postBody.operationName === stub.operation) {
console.log("STUBBING", stub.operation);
promise = Promise.resolve({
ok: true,
text() {
return Promise.resolve(JSON.stringify(stub.response));
}
});
return true;
}
return false;
});
}
if (promise) {
return promise;
}
console.log("Couldn't find a stub for the operation.");
return false;
});
}; Now in your specs, you can do stuff like this: const initialMocks = [usersQuery];
cy.visit("http://example.com/foobar", {
onBeforeLoad: fetch => graphqlFetch(fetch, initialMocks)
}); This'll load the usersQuery (or as many stubs as you want, which you can define in files (best way I've found to structure queries/mutations):
export const usersQuery = {
operation: "users",
response: {
data: {
users:
[
{
__typename: "FoobarUser",
id: 1,
username: "Baz"
}
]
}
}
} |
@joshuataylor nice. Thanks for doing that. Funny enough I modified the function to do this not too long after posting. Another thing i did was implement the clone method on the fetch response as some of our reporting tools rely on it |
@egucciar @joshuataylor very nice solution! It works for me. I just prefer to stick with custom command, so I don't have to explicitly export and import the function. If I have this in import './commands' Then I'll add this in // --------------------------------------
// Mock GraphQL requests with stubs.
// --------------------------------------
Cypress.Commands.add('mockGraphQL', stubs => {
cy.on('window:before:load', win => {
cy.stub(win, 'fetch', (...args) => {
console.log('Handling fetch stub', args)
const [url, request] = args
const postBody = JSON.parse(request.body)
let promise
if (url.indexOf('api') !== -1) {
stubs.some(stub => {
if (postBody.operationName === stub.operation) {
console.log('STUBBING', stub.operation)
promise = Promise.resolve({
ok: true,
text() {
return Promise.resolve(JSON.stringify(stub.response))
}
})
return true
}
return false
})
}
if (promise) {
return promise
}
console.log('Could not find a stub for the operation.')
return false
})
})
}) I write my stub in {
"operation": "allCars",
"response": {
"data": {
"allCars": [
{
"id": "cj1ds2x4snt290150j2qoa8tj",
"year": 2014,
"make": "Porsche",
"model": "918 Spyder"
},
{
"id": "cj1ds6g8pnzua0150ekfnmmwe",
"year": 2014,
"make": "McLaren",
"model": "P1"
},
{
"id": "cj1ds83ulnuxz01144k4rrwru",
"year": 2015,
"make": "Ferrari",
"model": "LaFerrari"
}
]
}
}
} And I can use the command in describe('my page', () => {
beforeEach(function() {
// Fetch fixtures.
cy.fixture('allCars').as('carsQuery')
})
context('mock & visit', () => {
beforeEach(function() {
cy.mockGraphQL([this.carsQuery])
cy.visit('http://localhost:8080')
})
it('my action', () => {})
})
}) It looks very neat. |
Is it possible to overwrite fixtures and/or operations? We are using different fixtures for the same operation. That operation can contain different data. |
@glenngijsberts I think we can extend the |
Recording a real graphql server's response requires that the query/mutation already be completed. This is undesirable for parallel workstreams, as we need to write our tests against a service that is not yet stood up. |
For those of you who cannot use the window.fetch stub method for graphql requests because you use apollo (which uses xhr) or any other client to make graphql requests, we've solved the issue this way. Instead of stubbing Cypress.on('window:before:load', (win) => {
let xml = win.XMLHttpRequest;
cy.stub(win, 'XMLHttpRequest').callsFake((...args) => {
let xmlObject = new xml(args);
let sendSuper = xmlObject.send.bind(xmlObject);
xmlObject.send = (...args) => {
if (xmlObject.url.endsWith('/api/graphql')) {
const requestBody = JSON.parse(args[0]);
Object.defineProperty(xmlObject, 'url', { value: xmlObject.url + '/' + requestBody.operationName });
}
return sendSuper(args);
};
return xmlObject;
});
}); Then add a command in
Then, in your tests you can call:
Hope this helps |
@brettjanderson is this still working for you? Cypress is not stubbing my xhr requests at all, I'm guessing that this is suppose to override the original xhr object but the stub doesn't seem to work for me, any ideas? |
https://www.npmjs.com/package/xhr-mock for those who need to xhr ^ this was a pretty good alternative. Created custom commands to use this for routes & custom gql server |
I've started a library to help with this: |
@garytryan How does it differ from this? https://github.com/tgriesser/cypress-graphql-mock |
Thanks for asking @vanbenj. That library seems to be unmaintained at this point. I was having an issues with the library, which was reported, and had a PR to fix, which is still unmerged. I really wanted to have a solution to this, that works. So I just made my own. |
just thinking out-loud here and not 100% sure about implementation (help welcome), but would something like this work? cy.server({
onAnyRequest(route, proxy) {
if (proxy.request.body.operationName==="GRAPH_QL_OPERATION")
ourRequestFound = true
}....
if(ourRequestFound){
cy.route("POST", "/graphql", "fixture:abc.json").as("graphql");
} Basically, only execute the route command to stub the response if the request operationName matched the one we are waiting for. |
@arkhamRejek did that package work for you? I tried it but I didn't have good luck |
The following solution worked for me (tested using XHR): commands.js Cypress.Commands.add('mockGraphQl', (operationName, fixture, method = 'POST') => {
let jsonData;
cy.fixture(fixture).then((fixture) => {
jsonData = fixture;
});
function patchXhrUsing(makeResponse) {
return (rawResponse) => {
const { xhr } = rawResponse;
Object.defineProperty(xhr.__proto__, 'response', { writable: true });
xhr.response = JSON.stringify(makeResponse(rawResponse));
return rawResponse;
};
}
cy.server();
cy.route({
url: '/graphql',
method: method,
onResponse: patchXhrUsing(rawResponse => {
if(rawResponse.request.body.operationName == operationName){
return jsonData;
}
}),
});
}); Using the command: I hope someone else finds this helpful |
I tried all the above solutions and nothing worked, finally came up with this one. Add command in Cypress.Commands.add('routeGraphQL', (operationName, response) => {
Cypress.on('window:before:load', (win) => {
win.XMLHttpRequest.prototype.open = (function (_super) {
return function () {
const [method, url, ...rest] = arguments;
let patchedUrl = url;
if (url.match('graphql$')) {
patchedUrl += `/${operationName}`;
}
const origResult = _super.apply(this, [method, patchedUrl, ...rest]);
return origResult;
};
})(win.XMLHttpRequest.prototype.open);
});
cy.server();
cy.route('POST', `/graphql/${operationName}`, response);
}); Structure your response data like this: const response = {
data: { ... }
} And finally use it: Cheers! |
@atfuentess I'm using the exact code. The call is intercepted I return the new body but it does not seem to work. The response body is not correct.. any idea? |
On a Next.js (latest) application with Apollo Client (latest), the following works when a single request is made in a test: First, in // Cypress cannot see or stub `fetch` network calls,
// temporary workaround is to delete/replace `window.fetch` so it
// falls back to XHR requests. See https://github.com/cypress-io/cypress/issues/95.
// Workaround at https://github.com/cypress-io/cypress-example-recipes/tree/master/examples/stubbing-spying__window-fetch
// does not work for Next.js 9.1.4 and up. Instead, use the following:
// https://github.com/zeit/next.js/issues/9483#issuecomment-567011326
Cypress.on('window:before:load', win => {
fetch('https://unpkg.com/unfetch/dist/unfetch.umd.js')
.then(stream => stream.text())
.then(response => {
win.eval(response);
// eslint-disable-next-line no-param-reassign
win.fetch = win.unfetch;
});
}); Then in a spec: cy.server();
// Using GraphQLZero, a free online fake GraphQL API
cy.route('POST', 'https://graphqlzero.almansi.me/api', 'fixture:todos.json').as(
'getTodos'
);
// [...]
cy.wait('@getTodos');
// [...] And this is the fixture file that matches the GraphQL API response. In {
"data": {
"todos": {
"__typename": "TodosPage",
"data": [
{
"__typename": "Todo",
"id": 1,
"title": "delectus aut autem",
"completed": false
},
{
"__typename": "Todo",
"id": 2,
"title": "quis ut nam facilis et officia qui",
"completed": false
},
{
"__typename": "Todo",
"id": 3,
"title": "fugiat veniam minus",
"completed": false
}
]
}
}
}
EDIT: Did not work for me if I have 2 |
We’ve faced these challenges in out development as well and I want to share our solution, inspired by a lot of the ideas in this discussion. We’ve come up with a way to:
The fixtures are recorded so that they return different data for the same request (for example before and after a mutation) just as the server did in record mode. Here’s the gist of it: Cypress.Commands.add('visitRoute', (url, operations = {}) => {
const operationIndices = {}
const serverStub = (_, req) => {
const { operationName } = JSON.parse(req.body)
if (operationIndices[operationName] !== undefined) {
operationIndices[operationName] += 1
} else {
operationIndices[operationName] = 0
}
const operationIndex = operationIndices[operationName]
const resultStub = operations[operationName][operationIndex]
if (resultStub) {
return Promise.resolve(responseStub(resultStub))
} else {
throw new Error('Unhandled fetch request that needs to be stubbed.')
}
}
const visitOptions =
Cypress.env('MODE') === 'replay'
? {
onBeforeLoad: win => {
cy.stub(win, 'fetch')
.callsFake(serverStub)
.as('fetch stub')
},
}
: {}
cy.visit(url, visitOptions)
}) I’ve created a simple runnable illustration of the approach here: I hope this is useful! |
I found it difficult to implement any of the solutions in this thread. Intercepting the XHR manually and injecting a response felt like too much when Cypress does provide most of that logic already. I decided to go with a different approach where I add the Example: // Add the operationName to all calls
// eg. /api/graphql?operation=GetUser
// In this case, we're using Apollo Client, but could be implemented otherwise
const graphqlLink = new HttpLink({
uri: `/api/graphql`,
fetch: (uri, options) => {
const { operationName } = JSON.parse(options.body);
return fetch(`${uri}?operation=${operationName}`, options);
}
}); beforeEach(function() {
cy.server();
// Stubbed REST request
cy.route('POST', '/api/layouts', 'fixture:layouts');
// Stubbed GraphQL request
cy.route('POST', '/api/gql?operation=GetUser', 'fixture:user');
});
it('should ...', () => {
// ...
}) This may not be ideal for everyone, but I hope it helps ✌️ [Edit] I've create a sample repo to demonstrate all the pieces to get this working: https://github.com/lukemartin/cypress-gql-stubbing |
After trying out several different ways mentioned in this thread, @lukemartin's example above was a great success but only after setting the I am using Apollo so it must be using Fetch under the hood instead of XHR. |
@lukemartin thanks for that, most elegant solution in this thread imo. Could you post your fixture files though? I cannot seem to get populated data through ApolloClient, I believe we need a |
@dan-cooke the fixture needs to be in the same shape that the real API would send:
I created a sample repo to demonstrate each part: https://github.com/lukemartin/cypress-gql-stubbing |
@lukemartin Thanks! Turns out my issue was not including Edit: |
@lukemartin Where are you using graphqlLink function in the repo? |
After trying all the above suggestions, I figured out following way and it works: Step 1: Step 2: Intercept fetch request and retrieve operation name. Call the original fetch by appending the operation name to the original url
Step 3: Intercept the required XHR with query as "/graphql?operation=${operationName}" and stub with the stub response
Step 4: In the test flow call |
Slight tweak on the solution above: in support/commands.js: import set from 'lodash/set';
Cypress.Commands.add('hookGraphQL', () => {
cy.on('window:before:load', (win) => {
const originalFetch = win.fetch;
const fetch = (path, options, ...rest) => {
if (options && options.body) {
try {
const body = JSON.parse(options.body);
if (body.operationName) {
return originalFetch(`${path}?operation=${body.operationName}`, options, ...rest);
}
} catch (e) {}
}
return originalFetch(path, options, ...rest);
};
cy.stub(win, 'fetch', fetch);
});
});
Cypress.Commands.add('mockGraphQL', (operationName, fixture, overrides = {}) => {
cy.fixture(`graphql/${fixture}.json`)
.then((json) => {
Object.entries(overrides).forEach(([objPath, objValue]) => set(json, objPath, objValue));
})
.as(operationName);
cy.route('POST', `**/v1/graphql/?operation=${operationName}`, `@${operationName}`);
}); in fixtures/graphql/BALANCE_MOCK.json: {
"data": {
"account": {
"balance": -5500
}
}
} In the test: describe('override graphql mock', () => {
beforeEach(() => {
cy.server();
cy.hookGraphQL();
cy.mockGraphQL('getAccountBalance', 'BALANCE_MOCK', { 'data.account.balance': 9999 });
});
it('displays the account balance of 99.99', () => {
...
});
}); |
With cypress 5.1, using the new route2 command it is very simple to mock a GraphQL request, for example:
I just added an if condition to evaluate if the body of the GraphQL request contains certain string as part of the query. |
@antonyfuentes how would you wait for a request to finish instead of mocking it? It's pretty easy with REST right now, harder with gql |
@antonyfuentes: I got a cors error, which was solved by adding an appropriate header:
|
Hey @yagudaev
https://docs.cypress.io/api/commands/route2.html#Waiting-on-a-request I hope that helps! |
I can recommend to switch to 5.6.0 if possible. My mocked tests results were very flaky in 5.3.0. This appears to be very much improved in the 5.6.0 version. |
Here is a potential solution to stubbing graphql requests in Cypress
Caveats
waited
on, but they do return promises that resolve immediately so it shouldn't be an issue in practicevisit
is required to set or change a stub, so this can have a negative performance impact on the testsfetch
but can probably be modified to work forxhr
Details
1. Create a Cypress
Command
to make life easier2. Create a mock GraphQL server (Optional)
You only need this if you want a fallback to a mock server. This requires your schema in SDL format to be exported as a string from a .js file. We handle this by generating the file before running Cypress using
get-graphql-schema
:The text was updated successfully, but these errors were encountered: