Cypress & GraphQL Response Mocking

Joshua Ross
Life at Paperless Post
6 min readSep 18, 2020

If you use Cypress and GraphQL in your project, you may want to test workflows that require mocking calls to GraphQL. There is no native support in Cypress for managing a GraphQL mock in an efficient way. Therefore, we need to create something custom. I will show a solution that I adapted from comments on this GitHub issue.

Defining my Criteria

When I first started, I wasn’t sure exactly how I wanted to configure GraphQL mocking. I knew I wanted to satisfy the following criteria:

  • Abstract the GraphQL Mock so test configuration was minimized and leverage Cypress functionality.
  • Allow the mock to be as specific or generic as a developer would like.
  • Properly track the order of calls to GraphQL to ensure reproducibility.

Cypress Commands

Cypress allows you to customize its cy object. You can override functions that exist or add new ones. At Paperless Post, we have several existing commands, such as a custom login function that mocks a user’s session cookie and JWT and another that triggers a hover state. We also have a shortcut to perform a click after x ms. To add support for GraphQL queries, we will create two new command functions.

If you don’t already have support for commands, you can enable it by creating a folder called support in your cypress directory. Then create an index.js and put your commands in it or create separate files for each of your commands and import them. More info on commands is available in the Cypress documentation: https://docs.cypress.io/api/cypress-api/custom-commands.html

The code that adds GraphQL support looks like this:

const GRAPHQL_URL = '/graphql';Cypress.Commands.add('mockGraphQLServer', () => {
// defined in this file. see next section of this blog post.
resetGraphQLMock();
cy.on('window:before:load', win => {
const originalFunction = win.fetch;
function fetch(path, request) {
if (path.includes(GRAPHQL_URL) && request.method === 'POST') {
// defined in this file. see next section of this blog post.
return processGraphQLResponse(request.body);
}
return originalFunction.apply(this, arguments);
}
cy.stub(win, 'fetch', fetch).as('graphqlStub');
});
});

This snippet does the following:

  1. Resets the GraphQL Mock requests tracker and response map.
  2. Before the window loads execute the following.
  3. Store the existing fetch for default behavior if not a GraphQL request.
  4. Create a new fetch method that checks if the request is for GraphQL and calls a handler we’ll create to process the quest. Otherwise, use the original fetch.
  5. Then overwrite the window’s fetch function with the newly created one and alias to graphqlStub. This alias is different than the ones used with cy.route.

If you aren’t 100% migrated to GraphQL, you can still mock other calls.

Caveat: These calls will not show up in the Network tab when running your cypress tests because we are not executing the calls using the polyfill fetch that you can configure in cypress.json using "experimentalFetchPolyfill": true. However, your implementation may be different. We are using Apollo Boost as our GQL client.

Add the GraphQL mock handler functions

What We Work With

The signature of the handler function is the same as a raw request to GraphQL. Looking at the request body to GraphQL in the Network tab of a browser, you’ll see the 3 variables accessible to use when mocking.

  • query — is the raw GraphQL Query you have defined in your code that is wrapped by the operation name. Variable names are not filled in. I don’t find this to be useful, but it’s good to know it’s there.
  • operationName — is what you’ve defined this query to be. It comes from the portion of a query that wraps the variables.
  • variables — the values that the GraphQL server will inject into the query to execute it.

Implementation

When determining how to respond to the request, I’ve created two maps to track all requests and responses.

// <String, Number>
let graphQLRequestMap = {};
// <String, Map<Number, JSON>>
let graphQLResponseMap = {};
  • graphQLRequestMap — uses a string as a key to track the number of executions of a request. The key is the GraphQL request’s operationName and variables objects run through JSON.stringify. Although, sometimes it will just be the operationName.
  • graphQLResponseMap — uses the same key, but has a second map that determines the response based on the number of times that key was called.

This is abstract, but will become clearer once you look at how they are used. The goal is to allow you to configure your mock as specifically or generically as you would like. Let’s take a look at how these are used.

const GLOBAL_LOOKUP = '*';

// <String, Number>
let gqlRequestMap = {};

// <String, Map<Number, JSON>>
let gqlResponseMap = {};

const resetGraphQLMock = () => {
gqlRequestMap = {};
gqlResponseMap = {};
};

const initializeRequestMap = (requestKey) => {
if (gqlRequestMap[requestKey] === undefined) {
gqlRequestMap[requestKey] = 0;
}
}

const responseMapHasKey = (request, count) =>
!gqlResponseMap[request] !== undefined &&
gqlResponseMap[request][count] !== undefined;

const getResponse = (request, count) => {
gqlRequestMap[request] += 1;
const result = gqlResponseMap[request][count];
return getResponseStub(result);
};

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

const getRequest = (requestJSON) => {
const request = JSON.stringify(requestJSON);
initializeRequestMap(request);
const counter = gqlRequestMap[request];
return { request, counter };
}

const processGraphQLResponse = (requestBody) => {
// `body` also contains `query` and we need to remove it
const { operationName, variables } = JSON.parse(requestBody);
// Return the most specific response we can find
const {
request: specificRequest,
counter: specificCounter
} = getRequest({ operationName, variables });
if (responseMapHasKey(specificRequest, specificCounter)) {
return getResponse(specificRequest, specificCounter);
}
if (responseMapHasKey(specificRequest, GLOBAL_LOOKUP)) {
return getResponse(specificRequest, GLOBAL_LOOKUP);
}
// Return a common response to all requests if it exists
const {
request: genericRequest,
counter: genericCounter
} = getRequest({ operationName });
if (responseMapHasKey(genericRequest, genericCounter)) {
return getResponse(genericRequest, genericCounter);
}
if (responseMapHasKey(genericRequest, GLOBAL_LOOKUP)) {
return getResponse(genericRequest, GLOBAL_LOOKUP);
}
console.log(
'Not found counter/operation/variables: ',
specificCounter,
operationName,
variables
);
return getResponseStub({});
};

Focusing on processGraphQLResponse, you can see that the goal is to load the specific response we can for a given GraphQL request. We start by looking for a response with the operationName and the variables that matches the specific counter of the request. If we don’t find one, we look for a global response. Then we fallback to a response that is associated with only the operationName, using the same specific counter or global lookup logic. This gives us the flexibility to respond appropriately to pagination operations or mutations and reloads.

Having the console.log is helpful while creating a test or making changes to a test after the application is modified.

Filling the Mock

Now that we know how both tracking maps are used, let’s look at how to fill the mock response data.

Cypress.Commands.add(
'addGraphQLServerResponse',
(requestDetailsInput, responseBody, allRequests = false) => {
let requestDetails;
if (typeof requestDetailsInput === 'string') {
requestDetails = JSON.stringify({
operationName: requestDetailsInput
});
} else {
requestDetails = JSON.stringify(requestDetailsInput);
}
if (allRequests) {
if (gqlResponseMap[requestDetails] === undefined) {
gqlResponseMap[requestDetails] = [];
}
gqlResponseMap[requestDetails][GLOBAL_LOOKUP] = responseBody;
} else if (gqlResponseMap[requestDetails] === undefined) {
gqlResponseMap[requestDetails] = [];
gqlResponseMap[requestDetails][0] = responseBody;
} else {
const array = gqlResponseMap[requestDetails];
gqlResponseMap[requestDetails][array.length] = responseBody;
}
}
);

Let’s look at the method signature first:

  1. requestDetailsInput — is either a string or json. If it is a string it should be the GraphQL operationName and this method will convert it into an object that is consistent with how the requests are managed. If it is json, we expect it to contain the operationName and optionally, the variables. However, if you don’t provide variables, you should pass the operationName as a string to keep your code cleaner.
  2. responseBody — is a JSON object (not a string) that contains the entire graphQL response. It can be stored in a separate file to keep your code clean, but you must load it prior to calling this. We haven’t replicated cy.route‘s support for file loading.
  3. allRequests — is a boolean that distinguishes if this configuration should be used if we don’t have anything more specific. The default is false.

Usage

Here is an example of how to use the new cypress commands.

import getMyStudentsResp from './my_students_response';
import getMyStudentsWithStudent3Resp from './my_students_response_with_3';
import DUMMY_STUDENT_DATA from './dummy_student_data';
const STUDENT_1_UUID = 'some-uuid-1';
const STUDENT_2_UUID = 'some-uuid-2';
const STUDENT_3_UUID = 'some-uuid-3';
const getStudent1Resp = { STUDENT_1_UUID, ...DUMMY_STUDENT_DATA };
const getStudent2Resp = { STUDENT_2_UUID, ...DUMMY_STUDENT_DATA };
const getStudent3Resp = { STUDENT_3_UUID, ...DUMMY_STUDENT_DATA };
context('Test Student List', () => {
beforeEach(() => {
cy.mockGraphQLServer();
cy.addGraphQLServerResponse('getMyStudents', getMyStudentsResp);
});
it('loads student 1 and 2 properly', () => {
cy.addGraphQLServerResponse({
operationName: 'getStudent',
variables: { studentId: STUDENT_1_UUID }
}, getStudent1Resp);
cy.addGraphQLServerResponse({
operationName: 'getStudent',
variables: { studentId: STUDENT_2_UUID }
}, getStudent2Resp);
// rest of the test...
});
it('adds student 3 and reload lists', () => {
cy.addGraphQLServerResponse({
operationName: 'addStudent',
variables: { studentId: STUDENT_3_UUID, ...DUMMY_STUDENT_DATA }
}, getStudent3Resp);
cy.addGraphQLServerResponse('getMyStudents', getMyStudentsWithStudent3Resp);
// rest of test...
});

});

And there you have it. One way to mock your GraphQL in Cypress. I hope this helps!

--

--