- Testing GraphQL calls
- Testing without network control
- route2
- Operation name
- Refactoring common code
- Inspecting requests
- Bonus 1 - number of requests
- Learn more
- Update 1: use GraphQL operation name as a header
Testing GraphQL calls
I have implemented a TodoMVC application using GraphQL backend: you can add new todos, mark them completed, and delete them.
All HTTP requests to the backend go through the same endpoint POST /
, making traditional stubbing using Cypress cy.route
command difficult.
In this blog post I will show how to spy on network calls using the new much more powerful command cy.route2. I don't know if this approach would become the official Cypress library for working with GraphQL; this is my own exploration. Previously I have explored stubbing the entire backend during testing, but I do see a value in using the actual backend during end-to-end testing.
Testing without network control
Before I start spying and stubbing network calls from the application to the backend, let's try writing a test that does NOT control the network, and cannot even observe the GraphQL network calls. Since we do not control the backend either, we are left with guesses about what data our application receives from the backend. Thus if we want to check how the user toggles a todo, we have to "play" against the user interface only. We cannot do specific actions during the test like:
- load 2 todos
- find the first todo
- assert it is not completed yet
- click on the checkbox
- the first todo should have class completed
Instead our test has to hope that there is at least a single todo returned by the backend. The test cannot assume the first item is NOT complete at the start; instead it only can check if the class changes after clicking on the checkbox.
1 | describe('TodoMVC', () => { |
The test is non-deterministic: notice how every run is either "incomplete -> complete Todo" or "complete -> incomplete Todo" scenario. Without tightly setting up the backend data and without spying or stubbing the network call fetching the data, the test has no way of performing the same actions.
route2
Recently we have introduced cy.route2 command that can intercept any HTTP call made by the application: it can intercept static resources like HTML and CSS, it can intercept any Ajax call (XMLHttpRequest
and fetch
). Thus we can build powerful abstractions on top of cy.route2
with the following main features:
- you can stub a call using
cy.route2(..., <response object>)
- you can inspect an outgoing request with
cy.route2(..., (req) => ... inspect req)
without stubbing it - you can stub a call using
cy.route2(..., (req) => ... req.reply(...))
which provides a way to do dynamic stubbing - you can inspect and even modify the server's response with
cy.route2(..., (req) => ... req.reply((res) => ...)
with the server's response being theres
variable
Let's use this to deal with GraphQL calls our application makes.
Operation name
Every GraphQL call I see in this application sends a JSON object in the request, and receives a JSON object back. Every request includes a field operationName
with values allTodos
, AddTodo
, updateTodo
, DeleteTodo
in our application.
Let's use the operation name to distinguish the network calls. Traditionally the REST API calls would have the HTTP method to do the same, but here we can spy on the request, parse it, and stub the first request that loads the data using operationName: allTodos
request. We will use the cy.route2 command to spy on the fetch POST /
request, but without stubbing it at first, just printing the request object sent to the server.
1 | describe('TodoMVC with GraphQL cy.route2', () => { |
The printed object shows the operation name, the query, and the empty variables object, since fetching all todos does not need to send any variables.
Let's stub the response - this will always load the same todos in the application, allowing us to reliably test completing an item user story:
1 | const allTodos = [ |
The test replies to GraphQL operations allTodos
with the same list of items. All other requests will be passed to the backend unchanged, since we did not execute the req.reply
call.
Let's finish writing the full test: we know the first item is incomplete; thus we can confirm the first list item gets the expected class when clicked.
1 | cy.route2(...) |
The test fails the last assertion - the Todo item nevers gets the expected completed
class for some reason.
Hmm, we see 2 GraphQL requests happening after clicking on the .toggle
checkbox. The application sends updateTodo
query and then fetches the updated list of todos. But we have stubbed the operationName: allTodos
and are returning the original list. An ideal test in my opinion would do the following in this case:
- stub the
operation: allTodos
request like we did to return known list of Todos - stub the
operation: UpdateTodo
request to confirm the right call to the backend is performed after clicking on the item - stub the
operation: allTodos
request with changed data matching what the server normally would return
We can add this logic to our test inside the cy.route2
handler:
1 | let allTodosCount = 0 |
The test now passes and works the same - and it never sends stray requests to the server (via updateTodo
request).
Since we can spy and stub GraphQL requests, our tests can do everything we might want from the Cypress Network Guide. But our test is pretty verbose. Can we do better?
Refactoring common code
Our cy.route2(...)
calls are very verbose - they parse the request, look at the operation name property, respond with an appropriate stub. We should factor out this logic into a nice little helper. I call my helper routeG
since it is route2 + GraphQL
logic. The main idea is to make it simple to deal with GraphQL operations, inspect the outgoing requests, and check the variables passed back and forth.
- We are constantly parsing the request and response JSON objects to check the operation name. The helper method should take care of parsing request bodies
- The call stub has to set the CORS headers on every request, we probably want to move these common headers into a factory method to avoid adding to every request
I called my library routeG
- similar to cy.route2
but meant for GraphQL. Here is how it works - you can use it directly to stub calls using operation names.
1 | import { routeG } from './routeG' |
Every time application calls GraphQL method with operationName: allTodos
our interceptor built on top of cy.route2
responds with the given list of todo items.
Since we pass the headers with every response, let's use another method routeG
exports to make a method that adds those headers automatically.
1 | import { initRouteG } from './routeG' |
Great. What about multiple queries? For example in our "complete todo" test we needed different responses to the allTodos
requests, and we needed a single response to the updateTodo
request. Sure - routeG
accepts an object with keys being the operations, and the values are either single stubs or lists of stubs for first call, second call, etc.
1 | it('completes the first todo', () => { |
Inspecting requests
When we test completing an item, we want to confirm the GraphQL calls going to the backend. It should have the right operation name, and have the right variables. Our helper routeG
takes core of it automatically - it records all GraphQL requests automatically. If we want to inspect them, just use the response
property from the routeG(...)
call.
1 | const { requests } = routeG({ ... }) |
All requests are saved by the operation name. We can see them all in the cy.wrap(requests)
object.
Bonus 1 - number of requests
When using cy.route
or cy.route2
the only way to confirm the number of requests was to wait for each one using cy.wait(<alias>)
. For example, to assert there were two Ajax calls:
1 | cy.route2(...).as('ajax') |
With routeG
and its requests
object, you simply assert the length of the array using cy.its
with its built-in retry-ability:
1 | const { requests } = routeG({...}) |
Learn more
I might play more with routeG
to see if I can write useful end-to-end tests for my web applications, and if it turns useful, I might move it from bahmutov/todo-graphql-example into its own NPM package. Meanwhile, check out:
- I have described my early attempts at stubbing GraphQL calls from Cypress tests in blog posts E2E Testing json-graphql-server using Cypress and Mocking GraphQL with Lunar in Cypress End-to-End Tests
- Tim Griesser maintains cypress-graphql-mock that might be long-term Cypress plugin for dealing with GraphQL
- just for kicks you might want to test React components that use GraphQL using cypress-react-unit-test which allows you to just specify a Mock GraphQL Provider around the mounted component; see example links in the repo
Update 1: use GraphQL operation name as a header
Using cy.intercept
you can match requests using the header field. You can apply a custom GraphQL header with the operation name and then match the calls easily
1 | cy.intercept({ |
Watch the video Set GraphQL Operation Name As Custom Header And Use It In cy.intercept and practice in my Cypress Network Testing Exercises course.