GraphQL stubbing using Cypress

Victor Vargas
Stuart Tech
Published in
4 min readFeb 7, 2023

We in the Test Engineering team at Stuart are very excited to share how we use Cypress as a testing framework on all our front-end applications.

There are several reasons why we want to do this, but these are our top two:

  • Today, UI tests are written in Java with Selenium and maintained by QA, which inhibits collaboration with other FE engineers as it’s not their main stack.
  • Stubbing network requests/responses are handled with an external mockserver.

How does Cypress solve these issues?

  • We can implement the tests with Typescript, the stack that our FE engineers use daily
  • Stubbing network requests/responses are handled internally.

Even though team collaboration is the key advantage of this change, there are other architectural advantages to Cypress — keep reading as we dig deeper into this stubbing topic:

In Selenium, test cases use the WebDriver component to communicate with the Browser Driver, which will then interact with the actual browser to execute the commands. Communications between all the components throughout this route are two-way, so that information can seamlessly flow back to the WebDriver from the actual browser. Likewise, developers will need different browser drivers for different types of browsers. Simply stated, Selenium runs outside the browser and executes the commands via the network.

In contrast, Cypress executes test cases directly inside the browser. A server process that powers Cypress makes it possible for Cypress to execute code in the same run loop as the application. Both Cypress and the server process constantly communicate with each other to perform tasks, enabling Cypress to respond to application events in real-time. This communication also allows Cypress to interact with OS components for tasks outside the browser, such as taking screenshots.

Cool, right?

To stub, we use cy.intercept() command.

As our FE application consumes a GraphQL API, we need to be able to stub it seamlessly. Here we see our first limitation.

Since GraphQL exposes a single resource with different queries/mutations, stubbing becomes more challenging. We will have to implement a stub for that exact resource.

cy.intercept({ method: "POST", url: "/gql" }, (req) => {
req.reply({fixture: "aResponseBody.json",});
});

The first attempt to stub a different query was to repeat that exact stub but change the response fixture.

cy.intercept({ method: "POST", url: "/gql" }, (req) => {
req.reply({fixture: "anotherResponseBody.json",});
});

However, since the matching criteria {method, url} are the same for requests, the first one will not be considered anymore (as per Cypress implementation, the last stub stays).

To solve this and to differentiate requests, we can use operationName, which is unique. The previous code will evolve as follows:

cy.intercept({ method: "POST", url: "/gql" }, (req) => {
if (req.body.operationName.includes("anOperationName")) {
req.reply({ fixture: "aResponseBody.json" });
}
if (req.body.operationName.includes("anotherOperationName")) {
req.reply({ fixture: "anotherResponseBody" });
}
})

If we want to deal with only a few requests, this approach is more than valid, but our Dashboard application, for example, produces many requests. This means that simply for logging, the code looks like:

cy.intercept({ method: "POST", url: "/gql" }, (req) => {
if (req.body.operationName.includes("GetClient")) {
req.reply({ fixture: "/gql/GetClient/corporateClient.json" });
}
if (req.body.operationName.includes("GetZonePackageTypes")) {
req.reply({ fixture: "/gql/getZonePackageTypes.json" });
}
if (req.body.operationName.includes("GetPackageSizes")) {
req.reply({ fixture: "/gql/getPackageSizes.json" });
}
if (req.body.operationName.includes("GetScheduledSlots")) {
req.reply({ fixture: "/gql/getScheduledSlots.json" });
}
if (req.body.operationName.includes("GetTransportTypes")) {
req.reply({ fixture: "/gql/getTransportTypes.json" });
}
if (req.body.operationName.includes("GetClosedZones")) {
req.reply({ fixture: "/gql/getClosedZones.json" });
}
if (req.body.operationName.includes("GetJobs")) {
req.reply({ fixture: "/gql/getJobs.json" });
}
if (req.body.operationName.includes("GetTermsAndConditions")) {
req.reply({ fixture: "/gql/getTermsAndConditions.json" });
}
if (req.body.operationName.includes("GetSavedPlaces")) {
req.reply({ fixture: "/gql/getSavedPlaces.json" });
}
if (req.body.operationName.includes("GetZones")) {
req.reply({ fixture: "/gql/getZones.json" });
}
if (req.body.operationName.includes("getClientTier")) {
req.reply({ fixture: "/gql/getClientTier.json" });
}
})

And the problem can (and will) snowball as soon as we want to either add a new stub or modify an existing one (Remember: we will need to execute the stub again with the new value).

Commands to the rescue!

We’ve have created a command that will store in a key-value pair the operation name with their corresponding responses that have already been registered:

const registeredResponses = new Map<string, string>();

Cypress.Commands.add(
"interceptGql",
(operationName: string, response: string) => {
registeredResponses.set(operationName, response);
cy.intercept({ method: "POST", url: "/gql" }, (req) => {
if (registeredResponses.has(operationName)) {
req.reply({
fixture: `/gql/${registeredResponses.get(
req.body.operationName
)}.json`,
});
}
});
}
);

This way, every time we want to add or overwrite an already stubbed response, we don’t need to mock it all at once; it will internally do so.

Imagine we want to change a stub during the test execution for the package sizes we have. The new implementation would look like this:

describe("/new screen tests", () => {
it("should stub different package sizes seamlessly", () => {
cy.interceptGql("GetPackageSizes", "getPackageSizes");
// Do something
// Do something else
// I write some more lines just in case Elon Musk acquires Stuart
// I don't want to be fired
// And more lines of code means better code
cy.interceptGql("GetPackageSizes", "getAnotherZonePackageSizes");
});
});

This is how we overcame this limitation. Do you have any other solution that you would like to share? Please share your thoughts by adding your comments!

Looking to join the Stuart team? Take a look at our open roles here.

--

--