🎬 That's a Wrap for GraphQLConf 2024! • Watch the Videos • Check out the recorded talks and workshops
DocumentationTesting Operations

Testing Operations

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:

  1. Build an executable schema: Use GraphQLSchema or a schema construction utility like makeExecutableSchema().
  2. Provide resolver implementations: Use real or mocked resolvers depending on what you’re testing.
  3. Set up a context if needed: Include things like authorization tokens, data loaders, or database access in the context.
  4. Call graphql() with your operation: Pass the schema, query or mutation, optional variables, and context.
  5. Validate the result: Assert that the data is correct and errors is either undefined or matches expected failure cases.

What you can test with operations

Use integration tests to verify:

  • Query and mutation flows
  • Variable handling and input validation
  • Nested field resolution
  • Error states and nullability
  • Real vs. mocked resolver behavior

These tests help ensure your GraphQL operations behave as expected across a wide range of scenarios.

Writing tests

Queries and mutations

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');

Variables

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

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);

Validating results

When validating results, test both data and errors:

  • Use toEqual for strict matches.
  • Use toMatchObject for partial structure checks.
  • Use toBeUndefined() or toHaveLength(n) for specific validations.
  • For large results, consider using snapshot tests.

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();

Using real data sources vs. mocked resolvers

You can run integration tests against real or mocked resolvers. The right approach depends on what you’re testing.

ApproachProsConsSetup
Real data sourcesCatches real bugs, validates resolver integration and schema usageSlower, needs data setup and teardownUse in-memory DB or test DB, reset state between tests
Mocked resolversFast, controlled, ideal for schema validationDoesn’t catch real resolver bugsStub 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.