Skip to content
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

Closed
davemcorwin opened this issue Oct 3, 2017 · 84 comments
Closed

A Solution for Stubbing GraphQL Requests #122

davemcorwin opened this issue Oct 3, 2017 · 84 comments

Comments

@davemcorwin
Copy link

davemcorwin commented Oct 3, 2017

Here is a potential solution to stubbing graphql requests in Cypress

Caveats

  • the requests cannot be waited on, but they do return promises that resolve immediately so it shouldn't be an issue in practice
  • a new visit is required to set or change a stub, so this can have a negative performance impact on the tests
  • assumes your app is using fetch but can probably be modified to work for xhr
  • assumes you are using Cypress v0.20.x (I think)
  • does not provide any error handling

Details

1. Create a Cypress Command to make life easier

/*
 * Create a command named `visitStubbed` that wraps the normal `cy.visit`. It
 * takes the url as the first parameter and * an object with your graphql
 * request stubs. Each key of the object must match the `operationName` of the
 * graphql request to be stubbed and each value is an object containing the
 * stubbed graphql response.
 *
 * Ex.
 * ```
 * cy.visitStubbed('/home', {
 *   fetchWidgets: {
 *     data: {
 *       widgets: [{
 *         id: 1,
 *         name: 'Cool Widget',
 *         __typename: 'Widget',
 *         //...
 *       }]
 *    }
 *  }
 * })
 * ```
 */

// import the function from the following step (optional)
import { runQuery } from '../support/graphql'

Cypress.Commands.add('visitStubbed', function(url, operations = {}) {
  cy.visit(url, {
    onBeforeLoad: win => {
      cy
        // stub `fetch`
        .stub(win, 'fetch')
        
        // your graphql endpoint
        .withArgs('/graphql')

        // call our stub
        .callsFake(serverStub)
    },
  })

  function serverStub(_, req) {
    // parse the request
    const { operationName, query, variables } = JSON.parse(req.body)

    // return the stub if it was provided
    const resultStub = operations[operationName]
    if (resultStub) {
      return Promise.resolve(responseStub(resultStub))
    }
    // else {
    //   return {} 
    // }

    // If you want, fallback to default mock data if stub for operation is not specified (optional)
    return runQuery(query, variables).then(responseStub)
  }
})

function responseStub(result) {
  return {
    json() {
      return Promise.resolve(result)
    },
    ok: true,
  }
}

2. 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:

ORIGIN=https://some-url.com

SCHEMA_FILE=cypress/schema.js

echo "module.exports = \`" > $SCHEMA_FILE
node_modules/.bin/get-graphql-schema $URL >> $SCHEMA_FILE
echo "\`" >> $SCHEMA_FILE
import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools'
import { graphql } from 'graphql'
// Your schema in SDL format exported as a string
import typeDefs from '../schema'

const schema = makeExecutableSchema({ typeDefs })
addMockFunctionsToSchema({ schema })

export function runQuery(query, variables) {
  return graphql(schema, query, {}, {}, variables)
}
@brian-mann
Copy link
Member

brian-mann commented Oct 3, 2017

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)

@brian-mann
Copy link
Member

brian-mann commented Oct 3, 2017

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.

@davemcorwin
Copy link
Author

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 server and route method of stubbing.

@Breuls
Copy link

Breuls commented Mar 13, 2018

This solution worked for our team, but with some slight alterations. I thought it would be useful to share that here.

The calls to fetch() didn't match until we removed the withArgs('/graphql') part. We fiddled around a bit with changing the way we define the path (with a slash, without one, with the schema and host name in front), but the serverStub() function was never called. So we just removed the withArgs:

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 _ was renamed to path and we used that to return a custom response for the non-graphql path that was called using fetch. It looks like this:

if (path === '/our-custom-non-graphql-path') {
  return Promise.resolve(responseStub({ foo: 'bar' }));
}

In addition, the responseStub() function didn't completely work for us, as Apollo (which we're using for GraphQL) apparently requires a text property which needs to be a function. To fix it, we made the function look like this:

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

We don't use the runQuery, by the way, so the end of our serverStub just returns an error:

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!

@jon-thompson
Copy link

Thanks so much for these solutions so far! I used a lot of the code in this discussion to create an entirely separate mockGraphQL command rather than creating an extended visit command:

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 mockGraphQL before the visit call in my test:

    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.

@canercandan
Copy link

canercandan commented Apr 5, 2018

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))
  })
})
  • stubbing an exact API URL thanks to .withArgs(API_URL),
  • everything is wrapped into a visitWithGraphQLStub cy command therefore you can call this command by doing `cy.visitWithGraphQLStub('url', { /* options */ })
  • using onBeforeLoad event to trigger the stub command
  • and also loading a fixture of graphql queries from ./fixtures/graphql.json, it can be defined as such:
{
  "queryName": {
    "data": {
      "queryCalled": {
         // data
      }
    }
  }
}

@bmakuh
Copy link

bmakuh commented Apr 27, 2018

Maybe I'm doing something wrong, but using cy.stub(win, 'fetch') doesn't quite work for us, because according to the stub docs:

Unlike most Cypress commands, cy.stub() is synchronous and returns a value (the stub) instead of a Promise-like chain-able object.
cy.stub() returns a Sinon.js stub. All methods found on Sinon.js spies and stubs are supported.

So if you replace fetch with something that's not then-able, if your app code uses .then() anywhere this will fail.

Fortunately all we had to do instead was just win.fetch = serverStub

@egucciar
Copy link

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.

@bmakuh
Copy link

bmakuh commented Apr 27, 2018

Hmm, I definitely must be doing something wrong then, because I keep getting cannot read property then of undefined errors when using stubs. Oh well.

@egucciar
Copy link

Paste your code?

@bmakuh
Copy link

bmakuh commented Apr 27, 2018

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 serverStub

@egucciar
Copy link

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.

@leovanhaaren
Copy link

@bmakuh did you solve it?

@MichaelHindley
Copy link

@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 cannot read property then of undefined

@MichaelHindley
Copy link

MichaelHindley commented May 14, 2018

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)

@MichaelHindley
Copy link

MichaelHindley commented May 15, 2018

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,
      ),
    )
  })
})

@mekhami
Copy link

mekhami commented May 17, 2018

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?

@ultrox
Copy link

ultrox commented Jun 12, 2018

Can anyone please provide minimal working solution, its kind of hard understanding this without in detail understanding how to setup cypress.

@egucciar
Copy link

I can paste a sample I got working shortly.

@egucciar
Copy link

egucciar commented Jun 12, 2018

@ultrox @mekhami

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);
  });
}

@ultrox
Copy link

ultrox commented Jun 13, 2018

@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.

@joshuataylor
Copy link

joshuataylor commented Jun 27, 2018

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):

usersQuery.js:

export const usersQuery = {
  operation: "users",
  response: {
    data: {
      users: 
      [
        {
          __typename: "FoobarUser",
          id: 1,
          username: "Baz"
        }
	  ]
	}
  }
}

@egucciar
Copy link

@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

@yaliv
Copy link

yaliv commented Aug 2, 2018

@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 support/index.js:

import './commands'

Then I'll add this in support/commands.js:

// --------------------------------------
// 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 fixtures/allCars.json:

{
  "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 integration/my-page.spec.js:

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.

@glenngijsberts
Copy link

Is it possible to overwrite fixtures and/or operations? We are using different fixtures for the same operation. That operation can contain different data.

@yaliv
Copy link

yaliv commented Dec 5, 2018

@glenngijsberts I think we can extend the mockGraphQL command to handle different query variables.
But I'm not sure we can handle different data for the same operation + variables like "data before mutation" and "data after mutation".

@slieker-d
Copy link

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.

@brettjanderson
Copy link

brettjanderson commented Jan 8, 2020

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 window.fetch, stub XMLHttpRequest instead:

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 commands.js like this:

Cypress.Commands.add('routeGraphQL', (operationName, response) => {
  cy.route('POST', `/api/graphql/${operationName}`, response);
});

Then, in your tests you can call:

cy.routeGraphQL('Apples', 'fixture:apples-response');

Hope this helps

@arkhamRejek
Copy link

@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?
& does this work for you on multiple requests ?

@arkhamRejek
Copy link

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

@garytryan
Copy link

garytryan commented Jan 21, 2020

I've started a library to help with this:
https://github.com/garytryan/mock-graphql-cypress.
https://www.npmjs.com/package/cy-mock-graphql

@vanbenj
Copy link

vanbenj commented Feb 7, 2020

@garytryan How does it differ from this? https://github.com/tgriesser/cypress-graphql-mock

@garytryan
Copy link

@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.

@gorbysbm
Copy link

gorbysbm commented Mar 26, 2020

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.

@antonyfuentes
Copy link

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

@arkhamRejek did that package work for you? I tried it but I didn't have good luck

@antonyfuentes
Copy link

antonyfuentes commented Mar 27, 2020

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:
cy.mockGraphQl('yourOperationName', 'yourFuxtureName');

I hope someone else finds this helpful

@onizuka87
Copy link

onizuka87 commented Apr 16, 2020

I tried all the above solutions and nothing worked, finally came up with this one.
Works with angular 9.1.0, apollo-angular 1.9.0, cypress 4.4.0 and XHR.

Add command in command.js:

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:
cy.routeGraphQL('operationName', response);

Cheers!

@rawul
Copy link

rawul commented Apr 21, 2020

@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?

@glambert
Copy link

glambert commented Apr 30, 2020

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/support/index.js:

// 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 cypress/fixtures/todos.json:

{
  "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
        }
      ]
    }
  }
}

Looking at #122 (comment) to mock multiple GraphQL API calls for a single test.

EDIT: Did not work for me if I have 2 useQuery calls on the same page. Will give https://github.com/garytryan/mock-graphql-cypress a try next 😄

@IvaKop
Copy link

IvaKop commented May 21, 2020

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:

  • Create tests in record mode where test fixtures are automatically generated from the server
  • Rerun tests in replay mode using the fixtures

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:
https://github.com/IvaKop/cypress-stub-graphql-requests

I hope this is useful!

@lukemartin
Copy link

lukemartin commented May 27, 2020

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 operationName to a query string for all GraphQL requests. This allows me to use Cypress's native stubbing solution, as we would with REST requests. This is especially helpful as our application needs both REST and GraphQL calls.

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

image

@tlaak
Copy link

tlaak commented Jul 1, 2020

After trying out several different ways mentioned in this thread, @lukemartin's example above was a great success but only after setting the "experimentalFetchPolyfill": truein cypress.json. It's available in Cypress 4.9.0.

I am using Apollo so it must be using Fetch under the hood instead of XHR.

@dan-cooke
Copy link
Contributor

dan-cooke commented Jul 16, 2020

@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 text property

@lukemartin
Copy link

@dan-cooke the fixture needs to be in the same shape that the real API would send:

{
  "data": {
    "users": [
	...
    ]
  }
}

I created a sample repo to demonstrate each part: https://github.com/lukemartin/cypress-gql-stubbing

@dan-cooke
Copy link
Contributor

dan-cooke commented Jul 16, 2020

@lukemartin Thanks! Turns out my issue was not including __typename

Edit:
For anyone stuck here I created a plugin for autostubbing fetch requests - we use it for our graphql + REST requests simultaneously
https://www.npmjs.com/package/cypress-autostub

@prashantabellad
Copy link

@lukemartin Where are you using graphqlLink function in the repo?

@prashantabellad
Copy link

prashantabellad commented Jul 24, 2020

After trying all the above suggestions, I figured out following way and it works:

Step 1:
This works with Cypress 4.9.0 and above
Set "experimentalFetchPolyfill": true in cypress.json file

Step 2: Intercept fetch request and retrieve operation name. Call the original fetch by appending the operation name to the original url

Cypress.Commands.add("mockGraphQL", () => {
  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);
        }
      }
      return originalFetch(path, options, ...rest);
    };
    cy.stub(win, "fetch", fetch);
  });
});

Step 3: Intercept the required XHR with query as "/graphql?operation=${operationName}" and stub with the stub response

Cypress.Commands.add("stubGraphQL", (operationName, stubResponse) => {
  cy.route("POST", `/graphql?operation=${operationName}`, stubResponse);
});

Step 4: In the test flow call
cy.stubGraphQL("operationName", stubResponseObject);

@iozzyi
Copy link

iozzyi commented Aug 26, 2020

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', () => {
    ...
  });
});

@antonyfuentes
Copy link

antonyfuentes commented Sep 9, 2020

With cypress 5.1, using the new route2 command it is very simple to mock a GraphQL request, for example:

cy.route2('/graphql', (req) => {
  if(req.body.includes('operationName')){
    req.reply({ fixture: 'mockData.json'});
  }
});

I just added an if condition to evaluate if the body of the GraphQL request contains certain string as part of the query.
If that is true, then I reply back with a custom body loaded from a fixture.

@yagudaev
Copy link

@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

@ruudgrosmann
Copy link

@antonyfuentes: I got a cors error, which was solved by adding an appropriate header:

if(req.body.includes('Search')){
    req.reply({ fixture: 'graphql/t.json',
                headers: {
                  'access-control-allow-origin': '*',
        }});

@antonyfuentes
Copy link

@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

Hey @yagudaev
This is what you're looking for (New in Cypress 5.6.0):

cy.route2('POST', '/graphql', (req) => {
  if (req.body.includes('mutation')) {
    req.alias = 'gqlMutation'
  }
})

// assert that a matching request has been made
cy.wait('@gqlMutation')

https://docs.cypress.io/api/commands/route2.html#Waiting-on-a-request

I hope that helps!

@ruudgrosmann
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests