Skip to content

docs: testing graphQL servers #4374

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: 16.x.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions website/pages/docs/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ const meta = {
'constructing-types': '',
'oneof-input-objects': '',
'defer-stream': '',
'-- 2.5': {
type: 'separator',
title: 'Testing',
},
'testing-graphql-servers': '',
'testing-approaches': '',
'testing-operations': '',
'testing-resolvers': '',
'testing-best-practices': '',
'-- 3': {
type: 'separator',
title: 'FAQ',
Expand Down
166 changes: 166 additions & 0 deletions website/pages/docs/testing-approaches.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
---
title: Testing Approaches
sidebarTitle: Testing Approaches
---

# Testing Approaches

Testing is essential for building reliable GraphQL servers. But not every test
gives you the same kind of feedback, and not every test belongs at every stage of development.
This guide explains the differences between unit tests, integration tests, and
end-to-end (E2E) tests, so you can choose the right approach for your project.

## Unit tests

Unit tests focus on testing resolver functions in isolation. You run the resolver directly
with mocked arguments and context. These tests do not involve your schema or run actual
GraphQL queries.

When you write unit tests, you’re checking that the resolver:

- Receives input arguments and context as expected
- Calls the correct business logic
- Handles errors properly
- Returns the correct result

### When to use unit tests

Unit tests are fast and provide tight feedback loops. They're especially useful when:

- Developing new resolvers
- Validating error handling and edge cases
- Refactoring resolver logic and needing immediate verification

However, unit tests have limitations. Because you're mocking inputs and skipping the schema
entirely, you won’t catch issues with how your schema connects to your resolver functions.
There's also a risk of false positives if your mocks drift from real usage over time.

### Example: Unit test for a resolver

This test verifies that the resolver produces the expected result using mocked inputs.

```javascript
const result = await myResolver(parent, args, context);
expect(result).toEqual(expectedResult);
```

## Integration tests

Integration tests go a step further by testing resolvers and the schema together.
You can run actual GraphQL queries and verify the end-to-end behavior within your
application's boundaries.

You can use the `graphql()` function from the GraphQL package, no HTTP server
needed. With the `graphql()` function, you can test the full flow: query > schema >
resolver > data source (mocked or real).

Integration tests confirm that:

- The schema is correctly wired to resolvers
- Resolvers behave as expected when called through a query
- Data sources return expected results

### When to use integration tests

Use integration tests when:

- You want to test the full operation flow from query to result
- You're testing how resolvers handle variables, fragments, and nested queries
- You want higher confidence that your schema and resolvers work together

Integration tests are slightly slower than unit tests but still fast enough for
regular development cycles, especially when you mock external data sources.

Trade-offs to consider:

- Confirms schema and resolver wiring
- Higher confidence than unit tests alone
- Requires more setup
- May miss production-specific issues such as network transport errors

> [!TIP]
>
> If you're preparing to onboard frontend clients or exposing your API to consumers,
> integration tests catch mismatches early before they reach production.

### Example: Integration test with `graphql()`

This test validates a user query with variables and mocked context.

```js
const query = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
`;

const result = await graphql({
schema,
source: query,
variableValues: { id: '123' },
contextValue: mockedContext, // mock database, authorization, loaders, etc.
});

expect(result.data).toEqual(expectedData);
```

## End-to-End (E2E) tests

E2E tests exercise the entire stack. With your server running and real HTTP
requests in play, you validate not just schema and resolver behavior, but also:

- HTTP transport
- Middleware such as authentication and logging
- Real data sources
- Infrastructure including networking and caching

E2E tests simulate production-like conditions and are especially valuable when:

- You're testing critical user flows end to end
- You want to validate authentication and authorization
- You need to test network-level behaviors such as timeouts and error handling
- You're coordinating multiple services together

E2E tests offer high confidence but come at the cost of speed and complexity.
They’re best used sparingly for critical paths, not as your primary testing approach.

Trade-offs to consider:

- Validates the full system in realistic conditions
- Catches issues unit and integration tests might miss
- Slower and resource-intensive
- Requires infrastructure setup

> [!NOTE]
>
> In the following guides, we focus on unit and integration tests. E2E tests are
> valuable, but they require different tooling and workflows.

## Comparing unit tests and integration tests

Unit and integration tests are complementary, not competing.

| Factor | Unit tests | Integration tests |
|:-------|:--------------------|:-------------------------|
| Speed | Fast | Moderate |
| Scope | Resolver logic only | Schema and resolver flow |
| Setup | Minimal | Schema, mocks, context |
| Best for | Isolated business logic, fast development loops | Verifying resolver wiring, operation flow |

Start with unit tests when building new features, then layer in integration tests
to validate schema wiring and catch regressions as your API grows.

## Choose a testing approach

There is no single correct approach to testing. Instead, a layered approach
works best. In general:

- Start with unit tests to move quickly and catch logic errors early
- Add integration tests to ensure schema and resolver wiring is correct
- Use E2E tests sparingly for high-confidence checks on critical flows

The goal is to build a safety net of tests that gives you fast feedback during
development and high confidence in production.
134 changes: 134 additions & 0 deletions website/pages/docs/testing-best-practices.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
---
title: Testing Best Practices
sidebarTitle: Testing Best Practices
---

# Testing Best Practices

As your GraphQL server grows, so does the risk of regressions, inconsistencies,
and slow development feedback. A thoughtful testing strategy helps you catch
problems early and ship with confidence—without overwhelming your team with
redundant or brittle tests.

This guide outlines practical testing patterns for GraphQL servers,
including schema safety, test coverage, data management, performance, and
continuous integration.

## Schema stability

Your schema is a contract with every client and consumer. Changes should
be intentional and reviewed.

### Best practices

- Use snapshot tests to catch unintended schema changes
– Tool: [`jest-serializer-graphql-schema`](https://github.com/kamilkisiela/jest-serializer-graphql-schema)
– Example:
```ts
expect(printSchema(schema)).toMatchSnapshot();
```
- Use schema diff tools in CI:
- `graphql-inspector`
- Apollo Rover
- GraphQL Hive
- Require review or approval for breaking changes
- Treat schema changes like semver: breaking changes should be explicit and
intentional

## Focus test coverage

You don’t need 100% coverage, you need meaningful coverage. Prioritize
behavior that matters.

### Best practices

- Test high-value paths:
- Business logic and custom resolver behavior
- Error cases: invalid input, auth failures, fallback logic
- Nullable fields and partial success cases
- Integration between fields, arguments, and data dependencies
- Coverage strategy:
- Unit test resolvers with significant logic
- Integration test operations end-to-end
- Avoid duplicating tests across layers
- Use tools to identify gaps:
- `graphql-coverage`
- Jest `--coverage`
- Static analysis for unused fields/resolvers

## Managing test data

Clean, flexible test data makes your tests easier to write, read, and
maintain.

### Best practices

- Use factories instead of hardcoding:

```ts
function createUser(overrides = {}) {
return { id: '1', name: 'Test User', ...overrides };
}

- Share fixtures:

```ts
export function createTestContext(overrides = {}) {
return {
db: { findUser: jest.fn() },
...overrides,
};
}

- Keep test data small and expressive
- Avoid coupling test data to real database structures
unless explicitly testing integration

## Keep tests fast and isolated

Slow tests kill iteration speed. Fast tests build confidence.

To keep tests lean:

- Use `graphql()` instead of spinning up a server
- Use in-memory or mock data—avoid real databases in most tests
- Group tests by concern: resolver, operation, schema
- Use parallelization (e.g., Jest, Vitest)
- Avoid shared state or global mocks that leak across test files

For large test suites:

- Split tests by service or domain
- Cache expensive steps where possible

## Integrate tests into CI

Tests are only useful if they run consistently and early.

### Best practices

- Run tests on every push or PR:
- Lint GraphQL files and scalars
- Run resolver and operation tests
- Validate schema via snapshot or diff
- Fail fast:
- Break the build on schema snapshot diffs
- Block breaking changes without a migration plan
- Use GitHub annotations or reporters to surface failures in PRs

## Lint your schema

Testing behavior is only the start. Clean, consistent schemas are
easier to maintain and consume.

Use schema linting to enforce:

- Descriptions on public fields and types
- Consistent naming and casing
- Deprecation patterns
- Nullability rules

Tools:

- `graphql-schema-linter`
- `eslint-plugin-graphql`
78 changes: 78 additions & 0 deletions website/pages/docs/testing-graphql-servers.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
title: Testing GraphQL Servers
sidebarTitle: Testing GraphQL Servers
---

# Testing GraphQL Servers

## Why testing matters

GraphQL's type system provides strong safety guarantees, but it
all reliable APIs need testing. The GraphQL compiler and runtime
enforce type correctness, and schema introspection helps clients
understand valid queries.

GraphQL can't protect you from:

- Databases returning incorrect data
- Resolvers throwing unexpected errors
- Integrations returning unexpected `null` values
- Schema changes breaking client applications

A robust testing strategy helps you scale and maintain your API in production.
Combining static and dynamic tests gives you confidence that your GraphQL server
behaves as expected.

## Risks of schema-first development

Schema-first development starts with designing your API upfront and letting
the schema drive implementation. This is a solid foundation, but treating
the schema as the only source of truth creates risks as your API evolves.

For example, if you rename a field, any client still expecting the original name
will break. Changing a field's type (like from `String` to `Int`) can also cause
unexpected failures in downstream clients. While GraphQL tooling, like schema
validation, helps enforce structure, it won't stop you from making breaking changes.

Schema changes may feel safe, but clients depend on that schema to remain
stable over time. Without tests or tracking, these changes can quickly go
unnoticed.

By testing schema changes and integrating schema tracking into your workflow,
you can catch breaking changes before they reach production. A strong testing strategy treats your schema as part of your system's contract.

## Common resolver issues

A correct schema doesn't guarantee safe execution. Resolvers are still a risk surface. Resolvers connect the schema to your business logic and data sources. They decide how data is fetched, transformed, and returned to clients.

Resolver errors are dangerous because failures often return `null` in part of the response, without failing the entire operation. Errors at the resolver level
don't necessarily break the whole response. Clients may receive incomplete data without realizing something went wrong. Tests should assert on complete and correct responses, not just that there was a response.

Unit tests of resolver ensure:

- Resolvers pass the correct inputs to your business logic.
- Resolvers return the expected outputs to the schema.
- Errors are surfaced clearly and handled predictably.

## Handling external dependencies

GraphQL servers often feel like the source of truth, but they're rarely the system of record. Your server talks to databases, internal APIs, and external third-party services.

External dependencies add complexity and risk. Even with a correct schema and resolvers, failures in upstream systems can disrupt your API. For
example:

- An unavailable database can cause the resolver to fail.
- A slow third-party API can lead to timeouts.
- An external service returning incomplete data can result in `null` values or
errors in your response.

APIs should fail in predictable ways. Good tests don't just check happy paths, they simulate timeouts, network failures, corrupted data, and empty responses. Building these scenarios into your testing strategy helps you catch issues early and keep your API reliable.

Beyond simulating failures, consider testing resilience patterns like retries or circuit breakers. These strategies help your API recover from transient failures and prevent cascading issues, especially in production environments.

## Next steps

- Learn different [testing approaches](\..\testing-approaches.mdx) to choose the right strategy for your project.
- Explore how to [test operations](\..\testing-operations.mdx) without running a server.
- Understand how to [test resolvers](\..\testing-resolvers.mdx) to catch logic errors early.
- Apply [best practices](\..\testing-best-practices.mdx) to scale testing to production.
Loading