Skip to content

Commit ba4b411

Browse files
docs: testing graphQL servers (#4374)
Co-authored-by: Benjie <[email protected]>
1 parent bb87e73 commit ba4b411

File tree

7 files changed

+760
-0
lines changed

7 files changed

+760
-0
lines changed

cspell.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ overrides:
2828
- codegen
2929
- composability
3030
- deduplication
31+
- Vitest
32+
- hardcoding
3133
- debuggable
3234
- subschema
3335
- subschemas

website/pages/docs/_meta.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ const meta = {
3636
'resolver-anatomy': '',
3737
'graphql-errors': '',
3838
'using-directives': '',
39+
'-- 3': {
40+
type: 'separator',
41+
title: 'Testing',
42+
},
43+
'testing-graphql-servers': '',
44+
'testing-approaches': '',
45+
'testing-operations': '',
46+
'testing-resolvers': '',
47+
'testing-best-practices': '',
3948
'authorization-strategies': '',
4049
'-- 4': {
4150
type: 'separator',
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
---
2+
title: Testing Approaches
3+
sidebarTitle: Testing Approaches
4+
---
5+
6+
import { Callout } from 'nextra/components'
7+
8+
# Testing Approaches
9+
10+
Testing is essential for building reliable GraphQL servers. But not every test
11+
gives you the same kind of feedback, and not every test belongs at every stage of development.
12+
This guide explains the differences between unit tests, integration tests, and
13+
end-to-end (E2E) tests, so you can choose the right approach for your project.
14+
15+
## Unit tests
16+
17+
Unit tests focus on testing resolver functions in isolation. You run the resolver directly
18+
with mocked arguments and context. These tests do not involve your schema or run actual
19+
GraphQL queries.
20+
21+
When you write unit tests, you’re checking that the resolver:
22+
23+
- Receives input arguments and context as expected
24+
- Calls the correct business logic
25+
- Handles errors properly
26+
- Returns the correct result
27+
28+
### When to use unit tests
29+
30+
Unit tests are fast and provide tight feedback loops. They're especially useful when:
31+
32+
- Developing new resolvers
33+
- Validating error handling and edge cases
34+
- Refactoring resolver logic and needing immediate verification
35+
36+
However, unit tests have limitations. Because you're mocking inputs and skipping the schema
37+
entirely, you won’t catch issues with how your schema connects to your resolver functions.
38+
There's also a risk of false positives if your mocks drift from real usage over time.
39+
40+
### Example: Unit test for a resolver
41+
42+
This test verifies that the resolver produces the expected result using mocked inputs.
43+
44+
```javascript
45+
const result = await myResolver(parent, args, context);
46+
expect(result).toEqual(expectedResult);
47+
```
48+
49+
## Integration tests
50+
51+
Integration tests go a step further by testing resolvers and the schema together.
52+
You can run actual GraphQL queries and verify the end-to-end behavior within your
53+
application's boundaries.
54+
55+
You can use the `graphql()` function from the GraphQL package, no HTTP server
56+
needed. With the `graphql()` function, you can test the full flow: request &gt; schema &gt;
57+
resolver &gt; data source (mocked or real).
58+
59+
Integration tests confirm that:
60+
61+
- The schema is correctly wired to resolvers
62+
- Resolvers behave as expected when called through a query
63+
- Data sources return expected results
64+
65+
### When to use integration tests
66+
67+
Use integration tests when:
68+
69+
- You want to test the full operation flow from request to result
70+
- You're testing how resolvers handle variables, fragments, and nested fields
71+
- You want higher confidence that your schema and resolvers work together
72+
73+
Integration tests are slightly slower than unit tests but still fast enough for
74+
regular development cycles, especially when you mock external data sources.
75+
76+
Trade-offs to consider:
77+
78+
- Confirms schema and resolver wiring
79+
- Higher confidence than unit tests alone
80+
- Requires more setup
81+
- May miss production-specific issues such as network transport errors
82+
83+
<Callout type="default">
84+
85+
If you're preparing to onboard frontend clients or exposing your API to consumers,
86+
integration tests catch mismatches early before they reach production.
87+
88+
</Callout>
89+
90+
### Example: Integration test with `graphql()`
91+
92+
This test validates a user query with variables and mocked context.
93+
94+
```js
95+
const query = `
96+
query GetUser($id: ID!) {
97+
user(id: $id) {
98+
id
99+
name
100+
}
101+
}
102+
`;
103+
104+
const result = await graphql({
105+
schema,
106+
source: query,
107+
variableValues: { id: '123' },
108+
contextValue: mockedContext, // mock database, authorization, loaders, etc.
109+
});
110+
111+
expect(result.data).toEqual(expectedData);
112+
```
113+
114+
## End-to-End (E2E) tests
115+
116+
E2E tests exercise the entire stack. With your server running and real HTTP
117+
requests in play, you validate not just schema and resolver behavior, but also:
118+
119+
- HTTP transport
120+
- Middleware such as authentication and logging
121+
- Real data sources
122+
- Infrastructure including networking and caching
123+
124+
E2E tests simulate production-like conditions and are especially valuable when:
125+
126+
- You're testing critical user flows end to end
127+
- You want to validate authentication and authorization
128+
- You need to test network-level behaviors such as timeouts and error handling
129+
- You're coordinating multiple services together
130+
131+
E2E tests offer high confidence but come at the cost of speed and complexity.
132+
They’re best used sparingly for critical paths, not as your primary testing approach.
133+
134+
Trade-offs to consider:
135+
136+
- Validates the full system in realistic conditions
137+
- Catches issues unit and integration tests might miss
138+
- Slower and resource-intensive
139+
- Requires infrastructure setup
140+
141+
<Callout type="info">
142+
143+
In the following guides, we focus on unit and integration tests. E2E tests are
144+
valuable, but they require different tooling and workflows.
145+
146+
</Callout>
147+
148+
## Comparing unit tests and integration tests
149+
150+
Unit and integration tests are complementary, not competing.
151+
152+
| Factor | Unit tests | Integration tests |
153+
|:-------|:--------------------|:-------------------------|
154+
| Speed | Fast | Moderate |
155+
| Scope | Resolver logic only | Schema and resolver flow |
156+
| Setup | Minimal | Schema, mocks, context |
157+
| Best for | Isolated business logic, fast development loops | Verifying resolver wiring, operation flow |
158+
159+
Start with unit tests when building new features, then layer in integration tests
160+
to validate schema wiring and catch regressions as your API grows.
161+
162+
## Choose a testing approach
163+
164+
There is no single correct approach to testing. Instead, a layered approach
165+
works best. In general:
166+
167+
- Start with unit tests to move quickly and catch logic errors early
168+
- Add integration tests to ensure schema and resolver wiring is correct
169+
- Use E2E tests sparingly for high-confidence checks on critical flows
170+
171+
The goal is to build a safety net of tests that gives you fast feedback during
172+
development and high confidence in production.
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
---
2+
title: Testing Best Practices
3+
sidebarTitle: Testing Best Practices
4+
---
5+
6+
# Testing Best Practices
7+
8+
As your GraphQL server grows, so does the risk of regressions, inconsistencies,
9+
and slow development feedback. A thoughtful testing strategy helps you catch
10+
problems early and ship with confidence—without overwhelming your team with
11+
redundant or brittle tests.
12+
13+
This guide outlines practical testing patterns for GraphQL servers,
14+
including schema safety, test coverage, data management, performance, and
15+
continuous integration.
16+
17+
## Schema stability
18+
19+
Your schema is a contract with every client and consumer. Changes should
20+
be intentional and reviewed.
21+
22+
### Best practices
23+
24+
- Use snapshot tests to catch unintended schema changes
25+
- Tool: [`jest-serializer-graphql-schema`](https://www.npmjs.com/package/jest-serializer-graphql-schema)
26+
- Example:
27+
```ts
28+
expect(schema).toMatchSnapshot();
29+
```
30+
- Consider sorting the schema to ensure a stable order of your schemas types, fields and arguments in the snapshots.
31+
```ts
32+
expect(lexicographicSortSchema(schema)).toMatchSnapshot();
33+
```
34+
- Use schema diff tools in CI:
35+
- `graphql-inspector`
36+
- Apollo Rover
37+
- GraphQL Hive
38+
- Require review or approval for breaking changes
39+
- Treat schema changes like semver: breaking changes should be explicit and
40+
intentional
41+
42+
## Focus test coverage
43+
44+
You don’t need 100% coverage, you need meaningful coverage. Prioritize
45+
behavior that matters.
46+
47+
### Best practices
48+
49+
- Test high-value paths:
50+
- Business logic and custom resolver behavior
51+
- Error cases: invalid input, auth failures, fallback logic
52+
- Nullable fields and partial success cases
53+
- Integration between fields, arguments, and data dependencies
54+
- Coverage strategy:
55+
- Unit test resolvers with significant logic
56+
- Integration test operations end-to-end
57+
- Avoid duplicating tests across layers
58+
- Use tools to identify gaps:
59+
- `graphql-coverage`
60+
- Jest `--coverage`
61+
- Static analysis for unused fields/resolvers
62+
63+
## Managing test data
64+
65+
Clean, flexible test data makes your tests easier to write, read, and
66+
maintain.
67+
68+
### Best practices
69+
70+
- Use factories instead of hardcoding:
71+
72+
```ts
73+
function createUser(overrides = {}) {
74+
return { id: '1', name: 'Test User', ...overrides };
75+
}
76+
```
77+
78+
- Share fixtures:
79+
80+
```ts
81+
export function createTestContext(overrides = {}) {
82+
return {
83+
db: { findUser: jest.fn() },
84+
...overrides,
85+
};
86+
}
87+
```
88+
89+
- Keep test data small and expressive
90+
- Avoid coupling test data to real database structures
91+
unless explicitly testing integration
92+
93+
## Keep tests fast and isolated
94+
95+
Slow tests kill iteration speed. Fast tests build confidence.
96+
97+
To keep tests lean:
98+
99+
- Use `graphql()` instead of spinning up a server
100+
- Use in-memory or mock data—avoid real databases in most tests
101+
- Group tests by concern: resolver, operation, schema
102+
- Use parallelization (e.g., Jest, Vitest)
103+
- Avoid shared state or global mocks that leak across test files
104+
105+
For large test suites:
106+
107+
- Split tests by service or domain
108+
- Cache expensive steps where possible
109+
110+
## Integrate tests into CI
111+
112+
Tests are only useful if they run consistently and early.
113+
114+
### Best practices
115+
116+
- Run tests on every push or PR:
117+
- Lint GraphQL files and scalars
118+
- Run resolver and operation tests
119+
- Validate schema via snapshot or diff
120+
- Fail fast:
121+
- Break the build on schema snapshot diffs
122+
- Block breaking changes without a migration plan
123+
- Use GitHub annotations or reporters to surface failures in PRs
124+
125+
## Lint your schema
126+
127+
Testing behavior is only the start. Clean, consistent schemas are
128+
easier to maintain and consume.
129+
130+
Use schema linting to enforce:
131+
132+
- Descriptions on public fields and types
133+
- Consistent naming and casing
134+
- Deprecation patterns
135+
- Nullability rules
136+
137+
Tools:
138+
139+
- `graphql-schema-linter`
140+
- `@graphql-eslint/eslint-plugin`

0 commit comments

Comments
 (0)