From 4fb470ddb1855da50e97da5bf634478bdbfb8f51 Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Mon, 21 Apr 2025 17:14:08 -0400 Subject: [PATCH 1/6] add testing graphQL servers docs --- website/pages/docs/testing-approaches.mdx | 167 +++++++++++++++ website/pages/docs/testing-best-practices.mdx | 127 ++++++++++++ .../pages/docs/testing-graphql-servers.mdx | 80 ++++++++ website/pages/docs/testing-operations.mdx | 153 ++++++++++++++ website/pages/docs/testing-resolvers.mdx | 193 ++++++++++++++++++ 5 files changed, 720 insertions(+) create mode 100644 website/pages/docs/testing-approaches.mdx create mode 100644 website/pages/docs/testing-best-practices.mdx create mode 100644 website/pages/docs/testing-graphql-servers.mdx create mode 100644 website/pages/docs/testing-operations.mdx create mode 100644 website/pages/docs/testing-resolvers.mdx diff --git a/website/pages/docs/testing-approaches.mdx b/website/pages/docs/testing-approaches.mdx new file mode 100644 index 0000000000..91c73b46b0 --- /dev/null +++ b/website/pages/docs/testing-approaches.mdx @@ -0,0 +1,167 @@ +--- +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 + +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 +appliaction's boundaries. + +You can use the `graphql()` function from the GraphQL pacakage, no HTTP server +needed. With the `graphql()` function, you can test the full flow: query > schema > +resolver > data source (mocked or real). + +With integration tests, you are verifiying 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 + +Integration tests are valuable 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 that your schema, resolver, and data sources work together +as expected: + +```js +const query = ` + query GetUser($id: ID!) { + user(id: $id) { + id + name + } + } +`; + +const result = await graphql({ + schema, + source: query, + variableValues: { id: '123' }, + contextValue: mockedContext, +}); + +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 behvaior, 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 | + +When starting a new project or feature, use unit tests to validate resolver +logic quickly. As your schema grows and you onboard consumers, add integration +tests to confirm that everything works end to end within your application. + +## When E2E tests add value + +E2E tests are slower and require a full environment setup, but they are essential when: +- Testing authentication and authorization flows +- Validating infrastructure behavior such as networking and retries +- Coordinating between multiple services and APIs + +Think of E2E tests as final rehearsals for your API. They’re not your first +line of defense, but they provide high confidence before deploying to production. + +## Choosing the right balance + +There is no one-size-fits-all 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 scehma 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. \ No newline at end of file diff --git a/website/pages/docs/testing-best-practices.mdx b/website/pages/docs/testing-best-practices.mdx new file mode 100644 index 0000000000..10a043142f --- /dev/null +++ b/website/pages/docs/testing-best-practices.mdx @@ -0,0 +1,127 @@ +--- +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 test 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 half the battle—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` \ No newline at end of file diff --git a/website/pages/docs/testing-graphql-servers.mdx b/website/pages/docs/testing-graphql-servers.mdx new file mode 100644 index 0000000000..9579df32e9 --- /dev/null +++ b/website/pages/docs/testing-graphql-servers.mdx @@ -0,0 +1,80 @@ +--- +title: Testing GraphQL Servers +sidebarTitle: Testing GraphQL Servers +--- + +import { Tabs } from 'nextra/components'; + +# 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. + +What GraphQL can't protect you from is: + +- 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 types of tests for GraphQL servers to choose the right strategy for your project. +- Explore how to test operations without running a server. +- Understand how to unit test resolvers to catch logic errors early. +- Apply best practices to scale testing to production. diff --git a/website/pages/docs/testing-operations.mdx b/website/pages/docs/testing-operations.mdx new file mode 100644 index 0000000000..fda1781c3d --- /dev/null +++ b/website/pages/docs/testing-operations.mdx @@ -0,0 +1,153 @@ +--- +title: Testing Operations +sidebarTitle: Testing 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. + +## Writing tests + +### Queries and mutations + +Use `graphql()` to run a GraphQL document string. Here's a basic test for a query: + +```ts +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: + +```ts +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: + +```ts +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: + +```ts +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: + +```ts +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. + +| 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. \ No newline at end of file diff --git a/website/pages/docs/testing-resolvers.mdx b/website/pages/docs/testing-resolvers.mdx new file mode 100644 index 0000000000..ff310443f9 --- /dev/null +++ b/website/pages/docs/testing-resolvers.mdx @@ -0,0 +1,193 @@ +--- +title: Testing Resolvers +sidebarTitle: Testing Resolvers +--- + +# Testing Resolvers + +Resolvers are the core of GraphQL execution. They bridge the schema with +your application logic and data sources. Unit testing resolvers helps you +validate the behavior of individual resolver functions in isolation, without +needing to run GraphQL operations or construct a full schema. + +Resolvers are good candidates for unit testing when they: + +- Contain business logic +- Handle conditional behavior or error states +- Call external services or APIs +- Perform transformations or mappings + +Unit tests for resolvers are fast, focused, and easy to debug. +They help you verify logic at the function level before wiring everything +together in integration tests. + +> **Tip:** Keep resolvers thin. Move heavy logic into service functions or +utilities that can also be tested independently. + +## Setup + +You don't need a schema or GraphQL server to test resolvers. You just need +a test runner and a function. + +### Test runners + +You can use any JavaScript or TypeScript test runner. Two popular options are: + +- `node:test` (built-in) + - Native test runner in Node.js 18 and later + - Use `--experimental-strip-types` if you're using TypeScript without + a transpiler + - Minimal setup + +```bash +node --test --experimental-strip-types +``` +- Jest + - Widely used across JavaScript applications + - Built-in support for mocks, timers, and coverage + - Better ecosystem support for mocking and configuration + +Choose the runner that best fits your tooling and workflow. +Both options support testing resolvers effectively. + +## Writing resolver tests + +Unit tests for resolvers treat the resolver as a plain function. +You provide the `args`, `context`, and `info`, then assert on the result. + +### Basic resolver function test + +You do not need the GraphQL schema or `graphql()` to write a basic resolver +function test, just call the resolver directly: + +```ts +function getUser(_, { id }, context) { + return context.db.findUserById(id); +} + +test('returns the expected user', async () => { + const context = { + db: { findUserById: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }) }, + }; + + const result = await getUser(null, { id: '1' }, context); + + expect(result).toEqual({ id: '1', name: 'Alice' }); +}); +``` + +### Async resolvers and Promises + +Always `await` the result of async resolvers or return the Promise from your test: + +```ts +test('resolves a user from async function', async () => { + const context = { + db: { find: async () => ({ name: 'Bob' }) }, + }; + + const result = await resolver(null, {}, context); + + expect(result.name).toBe('Bob'); +}); +``` + +### Error handling + +Resolvers often throw errors intentionally. Use `expect(...).rejects` to +test these cases: + +```ts +test('throws an error for missing user', async () => { + const context = { + db: { findUserById: jest.fn().mockResolvedValue(null) }, + }; + + await expect(getUser(null, { id: 'missing' }, context)) + .rejects + .toThrow('User not found'); +}); +``` + +Also consider testing custom error classes or error extensions. + +### Custom scalars + +Custom scalars often include serialization, parsing, and validation logic. +You can test them directly: + +```ts +import { GraphQLDate } from '../scalars/date.js'; + +test('parses ISO string to Date', () => { + expect(GraphQLDate.parseValue('2023-10-10')).toEqual(new Date('2023-10-10')); +}); + +test('serializes Date to ISO string', () => { + expect(GraphQLDate.serialize(new Date('2023-10-10'))) + .toBe('2023-10-10T00:00:00.000Z'); +}); +``` + +You can also test how your resolver behaves when working with scalar values: + +```ts +test('returns a serialized date string', async () => { + const result = await getPost(null, { id: '1' }, { + db: { + findPostById: () => ({ createdAt: new Date('2023-10-10') }), + }, + }); + + expect(result.createdAt).toBe('2023-10-10T00:00:00.000Z'); +}); +``` + +## Best practices for unit testing resolvers + +### Use dependency injection + +Resolvers often rely on a `context` object. In tests, treat it as an injected +dependency, not a global. + +Inject mock services, database clients, or loaders into `context`. This pattern +makes resolvers easy to isolate and test: + +```ts +const context = { + db: { + findUserById: jest.fn().mockResolvedValue(mockUser), + }, +}; +``` + +### Mocking vs. real data + +Use mocks to unit test logic without external systems. If you're testing logic +that depends on specific external behavior, use a stub or fake—but avoid hitting +real services in unit tests. Unit tests should not make real database or API +calls. Save real data for integration tests. + +### Testing resolver-level batching + +If you use `DataLoader` or a custom batching layer, test that batching works as +expected: + +```ts +const userLoader = { + load: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }), +}; + +const context = { loaders: { user: userLoader } }; + +// Make multiple resolver calls +await Promise.all([ + getUser(null, { id: '1' }, context), + getUser(null, { id: '1' }, context), +]); + +expect(userLoader.load).toHaveBeenCalledTimes(1); +``` + +You can also simulate timing conditions by resolving batches manually +or using fake timers in Jest. \ No newline at end of file From ec58803bdaf2686dd490ca129de3f6f5657d3dbd Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Mon, 21 Apr 2025 18:50:30 -0400 Subject: [PATCH 2/6] proofreading and typo cleanup --- website/pages/docs/testing-approaches.mdx | 47 +++++++++++-------- website/pages/docs/testing-best-practices.mdx | 17 +++++-- .../pages/docs/testing-graphql-servers.mdx | 2 +- website/pages/docs/testing-operations.mdx | 13 +++++ 4 files changed, 53 insertions(+), 26 deletions(-) diff --git a/website/pages/docs/testing-approaches.mdx b/website/pages/docs/testing-approaches.mdx index 91c73b46b0..e8991a9baf 100644 --- a/website/pages/docs/testing-approaches.mdx +++ b/website/pages/docs/testing-approaches.mdx @@ -5,28 +5,37 @@ 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. +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. +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 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. +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. +This test verifies that the resolver produces the expected result using mocked inputs. ```javascript const result = await myResolver(parent, args, context); @@ -37,13 +46,13 @@ expect(result).toEqual(expectedResult); 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 -appliaction's boundaries. +application's boundaries. -You can use the `graphql()` function from the GraphQL pacakage, no HTTP server +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). -With integration tests, you are verifiying that: +Integration tests confirm that: - The schema is correctly wired to resolvers - Resolvers behave as expected when called through a query @@ -51,7 +60,7 @@ With integration tests, you are verifiying that: ### When to use integration tests -Integration tests are valuable when: +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 @@ -74,8 +83,7 @@ Trade-offs to consider: ### Example: Integration test with `graphql()` -This test validates that your schema, resolver, and data sources work together -as expected: +This test validates a user query with variables and mocked context. ```js const query = ` @@ -91,7 +99,7 @@ const result = await graphql({ schema, source: query, variableValues: { id: '123' }, - contextValue: mockedContext, + contextValue: mockedContext, // mock database, authorization, loaders, etc. }); expect(result.data).toEqual(expectedData); @@ -100,7 +108,7 @@ 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 behvaior, but also: +requests in play, you validate not just schema and resolver behavior, but also: - HTTP transport - Middleware such as authentication and logging @@ -140,27 +148,26 @@ Unit and integration tests are complementary, not competing. | Setup | Minimal | Schema, mocks, context | | Best for | Isolated business logic, fast development loops | Verifying resolver wiring, operation flow | -When starting a new project or feature, use unit tests to validate resolver -logic quickly. As your schema grows and you onboard consumers, add integration -tests to confirm that everything works end to end within your application. +Start with unit tests when building new features, then layer in integration tests +to validate schema wiring and catch regressions as your API grows. ## When E2E tests add value -E2E tests are slower and require a full environment setup, but they are essential when: +E2E tests simulate full-stack conditions. Use them when: + - Testing authentication and authorization flows - Validating infrastructure behavior such as networking and retries - Coordinating between multiple services and APIs -Think of E2E tests as final rehearsals for your API. They’re not your first -line of defense, but they provide high confidence before deploying to production. +E2E tests are slower and require full setup, so reserve them for critical flows. ## Choosing the right balance -There is no one-size-fits-all approach to testing. Instead, a layered 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 scehma and resolver wiring is correct +- 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 diff --git a/website/pages/docs/testing-best-practices.mdx b/website/pages/docs/testing-best-practices.mdx index 10a043142f..21579c1ba8 100644 --- a/website/pages/docs/testing-best-practices.mdx +++ b/website/pages/docs/testing-best-practices.mdx @@ -64,11 +64,14 @@ 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 { @@ -76,16 +79,17 @@ maintain. ...overrides, }; } - ``` -- Keep test data small and expressive. + +- Keep test data small and expressive - Avoid coupling test data to real database structures -unless explicitly testing integration. +unless explicitly testing integration -## Keep test fast and isolated +## 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 @@ -93,6 +97,7 @@ To keep tests lean: - 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 @@ -113,15 +118,17 @@ Tests are only useful if they run consistently and early. ## Lint your schema -Testing behavior is only half the battle—clean, consistent schemas are +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` \ No newline at end of file diff --git a/website/pages/docs/testing-graphql-servers.mdx b/website/pages/docs/testing-graphql-servers.mdx index 9579df32e9..1c960cd7e9 100644 --- a/website/pages/docs/testing-graphql-servers.mdx +++ b/website/pages/docs/testing-graphql-servers.mdx @@ -14,7 +14,7 @@ all reliable APIs need testing. The GraphQL compiler and runtime enforce type correctness, and schema introspection helps clients understand valid queries. -What GraphQL can't protect you from is: +GraphQL can't protect you from: - Databases returning incorrect data - Resolvers throwing unexpected errors diff --git a/website/pages/docs/testing-operations.mdx b/website/pages/docs/testing-operations.mdx index fda1781c3d..02df6f5cbf 100644 --- a/website/pages/docs/testing-operations.mdx +++ b/website/pages/docs/testing-operations.mdx @@ -33,6 +33,19 @@ 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 From 989a0ddf509936fb8c0008e76f8314ea045a946e Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Mon, 21 Apr 2025 19:19:59 -0400 Subject: [PATCH 3/6] more proofreading, adding links, and reorganizing the left-hand nav --- website/pages/docs/_meta.ts | 9 +++++++++ website/pages/docs/testing-approaches.mdx | 14 +++----------- website/pages/docs/testing-graphql-servers.mdx | 10 ++++------ 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/website/pages/docs/_meta.ts b/website/pages/docs/_meta.ts index 39ac3a1486..686d5791d2 100644 --- a/website/pages/docs/_meta.ts +++ b/website/pages/docs/_meta.ts @@ -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', diff --git a/website/pages/docs/testing-approaches.mdx b/website/pages/docs/testing-approaches.mdx index e8991a9baf..f37b106fa2 100644 --- a/website/pages/docs/testing-approaches.mdx +++ b/website/pages/docs/testing-approaches.mdx @@ -23,6 +23,8 @@ When you write unit tests, you’re checking that the resolver: - 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 @@ -151,17 +153,7 @@ Unit and integration tests are complementary, not competing. Start with unit tests when building new features, then layer in integration tests to validate schema wiring and catch regressions as your API grows. -## When E2E tests add value - -E2E tests simulate full-stack conditions. Use them when: - -- Testing authentication and authorization flows -- Validating infrastructure behavior such as networking and retries -- Coordinating between multiple services and APIs - -E2E tests are slower and require full setup, so reserve them for critical flows. - -## Choosing the right balance +## Choose a testing approach There is no single correct approach to testing. Instead, a layered approach works best. In general: diff --git a/website/pages/docs/testing-graphql-servers.mdx b/website/pages/docs/testing-graphql-servers.mdx index 1c960cd7e9..4a1d1c81e6 100644 --- a/website/pages/docs/testing-graphql-servers.mdx +++ b/website/pages/docs/testing-graphql-servers.mdx @@ -3,8 +3,6 @@ title: Testing GraphQL Servers sidebarTitle: Testing GraphQL Servers --- -import { Tabs } from 'nextra/components'; - # Testing GraphQL Servers ## Why testing matters @@ -74,7 +72,7 @@ Beyond simulating failures, consider testing resilience patterns like retries or ## Next steps -- Learn different types of tests for GraphQL servers to choose the right strategy for your project. -- Explore how to test operations without running a server. -- Understand how to unit test resolvers to catch logic errors early. -- Apply best practices to scale testing to production. +- 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. \ No newline at end of file From 70606852d2ff716d878c60069ccdca0b216f5293 Mon Sep 17 00:00:00 2001 From: sarahxsanders Date: Tue, 13 May 2025 19:56:21 -0400 Subject: [PATCH 4/6] fix spellcheck --- cspell.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cspell.yml b/cspell.yml index ab32211f7c..4f577eecc2 100644 --- a/cspell.yml +++ b/cspell.yml @@ -27,6 +27,8 @@ overrides: - xlink - composability - deduplication + - Vitest + - hardcoding ignoreRegExpList: - u\{[0-9a-f]{1,8}\} From 131f70935822d5e93f19fc3ab5f4ee8aedd9f474 Mon Sep 17 00:00:00 2001 From: Benjie Date: Thu, 29 May 2025 16:37:12 +0100 Subject: [PATCH 5/6] Apply suggestions from code review --- website/pages/docs/testing-approaches.mdx | 8 ++++---- website/pages/docs/testing-best-practices.mdx | 20 +++++++++++-------- .../pages/docs/testing-graphql-servers.mdx | 12 +++++------ 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/website/pages/docs/testing-approaches.mdx b/website/pages/docs/testing-approaches.mdx index f37b106fa2..a5d44528e3 100644 --- a/website/pages/docs/testing-approaches.mdx +++ b/website/pages/docs/testing-approaches.mdx @@ -51,8 +51,8 @@ You can run actual GraphQL queries and verify the end-to-end behavior within you 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). +needed. With the `graphql()` function, you can test the full flow: request > schema > +resolver > data source (mocked or real). Integration tests confirm that: @@ -64,8 +64,8 @@ Integration tests confirm that: 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 to test the full operation flow from request to result +- You're testing how resolvers handle variables, fragments, and nested fields - You want higher confidence that your schema and resolvers work together Integration tests are slightly slower than unit tests but still fast enough for diff --git a/website/pages/docs/testing-best-practices.mdx b/website/pages/docs/testing-best-practices.mdx index 21579c1ba8..d0de16cb7b 100644 --- a/website/pages/docs/testing-best-practices.mdx +++ b/website/pages/docs/testing-best-practices.mdx @@ -22,11 +22,13 @@ 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) + – Tool: [`jest-serializer-graphql-schema`](https://www.npmjs.com/package/jest-serializer-graphql-schema) – Example: ```ts - expect(printSchema(schema)).toMatchSnapshot(); + expect(schema).toMatchSnapshot(); ``` + - Consider sorting the schema (`graphql.lexicographicSortSchema(schema)`) to ensure + a stable order of your schemas types, fields and arguments in the snapshots. - Use schema diff tools in CI: - `graphql-inspector` - Apollo Rover @@ -69,16 +71,18 @@ maintain. function createUser(overrides = {}) { return { id: '1', name: 'Test User', ...overrides }; } + ``` - Share fixtures: ```ts export function createTestContext(overrides = {}) { - return { - db: { findUser: jest.fn() }, - ...overrides, - }; -} + return { + db: { findUser: jest.fn() }, + ...overrides, + }; + } + ``` - Keep test data small and expressive - Avoid coupling test data to real database structures @@ -131,4 +135,4 @@ Use schema linting to enforce: Tools: - `graphql-schema-linter` -- `eslint-plugin-graphql` \ No newline at end of file +- `@graphql-eslint/eslint-plugin` \ No newline at end of file diff --git a/website/pages/docs/testing-graphql-servers.mdx b/website/pages/docs/testing-graphql-servers.mdx index 4a1d1c81e6..03dfbf15a3 100644 --- a/website/pages/docs/testing-graphql-servers.mdx +++ b/website/pages/docs/testing-graphql-servers.mdx @@ -7,7 +7,7 @@ sidebarTitle: Testing GraphQL Servers ## Why testing matters -GraphQL's type system provides strong safety guarantees, but it +GraphQL's type system provides strong safety guarantees, but all reliable APIs need testing. The GraphQL compiler and runtime enforce type correctness, and schema introspection helps clients understand valid queries. @@ -31,7 +31,7 @@ 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 +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 @@ -72,7 +72,7 @@ Beyond simulating failures, consider testing resilience patterns like retries or ## 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. \ No newline at end of file +- 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. \ No newline at end of file From cd00ddf6a35f541fc34e35cac22337adee59c8b0 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 29 May 2025 16:44:37 +0100 Subject: [PATCH 6/6] Fix various formatting issues --- website/pages/docs/testing-approaches.mdx | 24 ++++++++++++------- website/pages/docs/testing-best-practices.mdx | 12 ++++++---- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/website/pages/docs/testing-approaches.mdx b/website/pages/docs/testing-approaches.mdx index a5d44528e3..6b30e92c12 100644 --- a/website/pages/docs/testing-approaches.mdx +++ b/website/pages/docs/testing-approaches.mdx @@ -3,6 +3,8 @@ title: Testing Approaches sidebarTitle: Testing Approaches --- +import { Callout } from 'nextra/components' + # Testing Approaches Testing is essential for building reliable GraphQL servers. But not every test @@ -78,10 +80,12 @@ Trade-offs to consider: - 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. + + + 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()` @@ -134,10 +138,12 @@ Trade-offs to consider: - 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. + + + 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 @@ -163,4 +169,4 @@ works best. In general: - 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. \ No newline at end of file +development and high confidence in production. diff --git a/website/pages/docs/testing-best-practices.mdx b/website/pages/docs/testing-best-practices.mdx index d0de16cb7b..94f04dae5e 100644 --- a/website/pages/docs/testing-best-practices.mdx +++ b/website/pages/docs/testing-best-practices.mdx @@ -22,13 +22,15 @@ be intentional and reviewed. ### Best practices - Use snapshot tests to catch unintended schema changes - – Tool: [`jest-serializer-graphql-schema`](https://www.npmjs.com/package/jest-serializer-graphql-schema) - – Example: + - Tool: [`jest-serializer-graphql-schema`](https://www.npmjs.com/package/jest-serializer-graphql-schema) + - Example: ```ts expect(schema).toMatchSnapshot(); ``` - - Consider sorting the schema (`graphql.lexicographicSortSchema(schema)`) to ensure - a stable order of your schemas types, fields and arguments in the snapshots. + - Consider sorting the schema to ensure a stable order of your schemas types, fields and arguments in the snapshots. + ```ts + expect(lexicographicSortSchema(schema)).toMatchSnapshot(); + ``` - Use schema diff tools in CI: - `graphql-inspector` - Apollo Rover @@ -135,4 +137,4 @@ Use schema linting to enforce: Tools: - `graphql-schema-linter` -- `@graphql-eslint/eslint-plugin` \ No newline at end of file +- `@graphql-eslint/eslint-plugin`