website/pages/docs/testing-operations.mdx
Integration tests are the most effective way to test GraphQL operations. These tests run actual queries and mutations against your schema and resolvers to verify the full execution path. They validate how operations interact with the schema, resolver logic, and any connected systems.
Compared to unit tests, which focus on isolated resolver functions, integration tests exercise the full request flow. This makes them ideal for catching regressions in schema design, argument handling, nested resolution, and interaction with data sources.
To run integration tests, you don’t need a running server.
Instead, use the graphql() function from graphql-js, which
directly executes operations against a schema.
Integration testing for operations includes the following steps:
GraphQLSchema or a schema
construction utility like makeExecutableSchema().graphql() with your operation: Pass the schema, query or mutation,
optional variables, and context.data is correct and errors
is either undefined or matches expected failure cases.Use integration tests to verify:
These tests help ensure your GraphQL operations behave as expected across a wide range of scenarios.
Use graphql() to run a GraphQL document string. Here's a basic test for a query:
const result = await graphql({
schema,
source: 'query { user(id: "1") { name } }',
});
expect(result.errors).toBeUndefined();
expect(result.data?.user.name).toBe('Alice');
For mutations, the structure is the same:
const source = `
mutation {
createUser(input: { name: "Bob" }) {
id
name
}
}
`;
const result = await graphql({ schema, source });
expect(result.errors).toBeUndefined();
expect(result.data?.createUser.name).toBe('Bob');
Use the variableValues option to test operations that accept input:
const result = await graphql({
schema,
source: `
query GetUser($id: ID!) {
user(id: $id) {
name
}
}
`,
variableValues: { id: '1' },
});
Nested queries validate how parent and child resolvers interact. This ensures the response shape aligns with your schema and the data flows correctly through resolvers:
const result = await graphql({
schema,
source: `
{
user(id: "1") {
name
posts {
title
}
}
}
`,
});
expect(result.errors).toBeUndefined();
expect(result.data?.user.posts).toHaveLength(2);
When validating results, test both data and errors:
toEqual for strict matches.toMatchObject for partial structure checks.toBeUndefined() or toHaveLength(n) for specific validations.You can also test failure modes:
const result = await graphql({
schema,
source: `
query {
user(id: "nonexistent") {
name
}
}
`,
});
expect(result.errors).toBeDefined();
expect(result.data?.user).toBeNull();
You can run integration tests against real or mocked resolvers. The right approach depends on what you're testing.
| Approach | Pros | Cons | Setup |
|---|---|---|---|
| Real data sources | Catches real bugs, validates resolver integration and schema usage | Slower, needs data setup and teardown | Use in-memory DB or test DB, reset state between tests |
| Mocked resolvers | Fast, controlled, ideal for schema validation | Doesn’t catch real resolver bugs | Stub resolvers or inject mocks into context or resolver |
Use mocked resolvers when you're testing schema shape, field availability, or operation structure in isolation. Use real data sources when testing application logic, integration with databases, or when preparing for production-like behavior.