Make GraphQL Calls From Cypress Tests

How to make GraphQL calls from Cypress tests using the cy.request command

🧭 Find the source code for this blog post in the repository bahmutov/todo-graphql-example

Fetch all todos

First, let's see how our Cypress tests can fetch all todo items. This allows us to confirm what the application is showing for example. Let's fetch the items using the cy.request command.

cypress/integration/request-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
it('fetches all items', () => {
cy.request({
method: 'POST',
url: 'http://localhost:3000/',
body: {
operationName: 'allTodos',
query: `
query allTodos {
allTodos {
id,
title,
completed
}
}
`,
},
})
})

Tip: if you are not sure about the body object, look at the network calls the application is making, and copy the GraphQL query from there.

Request list of todos

From the response object we can get the list of todos and confirm their number and other details.

1
2
3
cy.request({ ... })
.its('body.data.allTodos')
.should('have.length', 2)

Note, if you do not know the precise number of items, but there should be >= 0 items use .gte assertion

1
2
3
cy.request({ ... })
.its('body.data.allTodos')
.should('have.length.gte', 0)

Use application client

Instead of using cy.request directly, we can fetch the items using the same GraphQL client the application is using! Just make sure to set the "cache" to false to avoid race conditions between the application and the test runner's client's memory caches.

Let's say this is the source file with the GraphQL client exported

src/graphql-client.js
1
2
3
4
5
6
7
8
// imports and init code
export const client = new ApolloClient({
link: concat(operationNameLink, httpLink),
fetchOptions: {
mode: 'no-cors',
},
cache: new InMemoryCache(),
})

Then we can create an instance of the GraphQL client by importing it from the spec file. Note: this creates a separate client instance from the application's GraphQL client instance.

cypress/integration/request-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { gql } from '@apollo/client'
import { client } from '../../src/graphql-client'

it('fetches all items using application client', () => {
// make a GraphQL query using the app's client
// https://www.apollographql.com/docs/react/data/queries/
const query = gql`
query allTodos {
allTodos {
id
title
completed
}
}
`

// use https://on.cypress.io/wrap to let the Cypress test
// wait for the promise returned by the "client.query" to resolve
cy.wrap(
client.query({
query,
// it is important to AVOID any caching here
// and fetch the current server data
fetchPolicy: 'no-cache',
}),
)
.its('data.allTodos')
.should('have.length', 2)
})

The test passes

Request the list of todos using the loaded GraphQL client

Add an item

Using the GraphQL client and even sharing the queries between the application and the specs is very convenient. For example, let's create an item and then confirm it is visible in the application.

cypress/integration/request-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { gql } from '@apollo/client'
import { client } from '../../src/graphql-client'

it('creates one item', () => {
const random = Cypress._.random(1e5)
const title = `test ${random}`
cy.log(`Adding item ${title}`)
.then(() => {
const query = gql`
mutation AddTodo($title: String!) {
createTodo(title: $title, completed: false) {
id
}
}
`

// by returning the promise returned by the "client.query"
// call from the .then callback, we force the test to wait
// and yield the result to the next Cypress command or assertion
return client.query({
query,
variables: {
title,
},
// it is important to AVOID any caching here
// and fetch the current server data
fetchPolicy: 'no-cache',
})
})
// use zero timeout to avoid "cy.its" retrying
// since the response object is NOT going to change
.its('data.createTodo', { timeout: 0 })
.should('have.property', 'id')

// the item we have created should be shown in the list
cy.visit('/')
cy.contains('.todo', title)
})

Created an item using GraphQL mutation

Share GraphQL client

If the spec file imports the GraphQL client from the application's source file, it creates its own instance separate from the GraphQL client created by the application in its iframe. This has some advantages, for example, the test above could execute the GraphQL mutation even before the cy.visit command loaded the application. But if you want to share the GraphQL client between the application and the spec, there is a way:

1
2
3
4
5
6
7
export const client = new ApolloClient({
...
}

if (window.Cypress) {
window.graphqlClient = client
}

We set the client reference created by the application as a property of the window object. From the spec, we can grab this property and use it to spy and stub client method calls. Here is a typical test that:

  1. Visits the page. The application creates a GraphQL client object and sets it as window.graphqlClient value.
  2. The command cy.visit yields the application' window object. Thus we can directly retry until we get the client's reference using cy.visit('/').should('have.property', 'graphqlClient') assertion.
  3. Once we have an object reference, we can use cy.spy and cy.stub to observe / stub the calls the application is making.
cypress/integration/spy-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
it('adds a todo', () => {
// set up the spy on "client.mutate" method
cy.visit('/')
.should('have.property', 'graphqlClient')
.then((client) => {
cy.spy(client, 'mutate').as('mutate')
})
// have the application make the call by using the UI
cy.get('.new-todo').type('Test!!!!{enter}')
// confirm the call has happened with expected variables
cy.get('@mutate')
.should('have.been.calledOnce')
.its('firstCall.args.0.variables')
.should('deep.include', {
title: 'Test!!!!',
})
.and('have.property', 'id')
})

Spy on the client.mutate method call

Aliases

Tip: we gave our spy an alias "mutate" using the cy.as command. We can get these aliases using the test context "this[alias name]" syntax after the .as(name) command has finished. Cypress forces all commands to run one by one, thus we can use the "this[alias name]" syntax by making the access from a .then(function () { ... }) callback function following the .as command.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
it('adds a todo (alias)', () => {
// set up the spy on "client.mutate" method
cy.visit('/')
.should('have.property', 'graphqlClient')
.then((client) => {
// once the ".as" command finishes
// we can access the spy using the "this.mutate" property
cy.spy(client, 'mutate').as('mutate')
})
// have the application make the call by using the UI
cy.get('.new-todo')
.type('Test!!!!{enter}')
// note the "function () { ... }" syntax is used to
// make sure the "this" points at the test context object
.then(function () {
// confirm the call has happened with expected variables
// by now the client.mutate has been called,
// and the alias has been set (no retries for here)
expect(this.mutate).to.have.been.calledOnce
expect(this.mutate.firstCall.args[0].variables)
.to.deep.include({
title: 'Test!!!!',
})
.and.to.have.property('id')
})
})

Since the expect(this.mutate).to.have.been.calledOnce does not retry make sure to use it only after the client call has been made for sure.

Use test context to access the aliased spy

Delete all todos

A very common problem for an end-to-end test is to clear the existing data before the test. Imagine you have a GraphQL endpoint, and you could get all Todo items? How would you go about deleting them? If you do not have a mutation "delete all X", then you need to delete each item one by one.

Here is how to do this: first let's write a reusable method and place it in the utils.js file.

cypress/integration/utils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { gql } from '@apollo/client'
import { client } from '../../src/graphql-client'

export function deleteAll() {
// fetches all todo items, grabs their IDs, and deletes them
cy.log('**deleteAll**')
.then(() =>
client.query({
// it is important to AVOID any caching here
// and fetch the current server data
fetchPolicy: 'no-cache',
query: gql`
query getAllTodos {
allTodos {
id
}
}
`,
}),
)
.its('data.allTodos')
// from each item, grab just the property "id"
.then((items) => Cypress._.map(items, 'id'))
.then((ids) => {
if (!ids.length) {
cy.log('Nothing to delete')
return
}
cy.log(`Found **${ids.length}** todos`)

// delete all items one by one
ids.forEach((id) => {
const mutation = gql`
mutation deleteTodo {
removeTodo(id: "${id}") {
id
}
}
`
cy.log(`deleting item id:**${id}**`).then(
() =>
client.mutate({
mutation,
}),
{ log: false },
)
})
})
}

We can use this method before each test, or whenever we want:

cypress/integration/delete-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <reference types="cypress" />
import { deleteAll } from './utils'

describe('Delete items', () => {
beforeEach(deleteAll)

it('deletes all items by making GraphQL calls', () => {
cy.intercept({
method: 'POST',
url: '/',
headers: {
'x-gql-operation-name': 'allTodos',
},
}).as('allTodos')

cy.visit('/')
cy.wait('@allTodos').its('response.body.data.allTodos').should('be.empty')
cy.get('.todo').should('have.length', 0)
})
})

Deleting each item one by one

You can see deleting all items using GraphQL calls in this video below

Happy testing!