Testing Resolvers and Operations

Advanced
13 min

Testing Resolvers and Operations

A GraphQL API has clear seams for testing: resolvers are plain functions and the schema can execute operations without a network. After this lesson you will be able to unit-test resolvers in isolation, run whole operations against an in-process server with a fake context, mock a schema for frontend tests, and catch breaking schema changes automatically.

Three Levels of Tests

| Level | What runs | Speed | Catches | |---|---|---|---| | Unit | One resolver function with a fake context | Fastest | Business logic bugs | | Operation | Parse, validate and execute a document in-process | Fast | Schema and resolver wiring, nullability, auth rules | | HTTP | The real server over supertest or fetch | Slower | Middleware, headers, CORS, error formatting |

Most of your tests belong in the first two levels; a handful of HTTP tests confirm the plumbing.

Unit-Testing a Resolver

Resolvers receive everything through arguments, so a test can call one directly (the examples use Vitest; Jest is identical):

typescript
import { describe, it, expect } from "vitest"; import { resolvers } from "../src/resolvers"; describe("Mutation.deletePost", () => { it("rejects a user who does not own the post", async () => { const ctx = { user: { id: "2", role: "USER" }, db: { posts: { find: async () => ({ id: "1", authorId: "9" }), delete: async () => {} } }, }; await expect(resolvers.Mutation.deletePost({}, { id: "1" }, ctx, {} as never)) .rejects.toMatchObject({ extensions: { code: "FORBIDDEN" } }); }); });

No server, no network: the test runs in a millisecond and pinpoints the failing rule.

Executing Operations Against the Schema

Apollo Server's executeOperation runs a full operation without an HTTP listener. The contextValue option replaces the context function, so authentication and database access are under the test's control:

typescript
import assert from "node:assert"; import { ApolloServer } from "@apollo/server"; import { typeDefs, resolvers } from "../src/schema"; const server = new ApolloServer({ typeDefs, resolvers }); it("returns a post with its author", async () => { const db = { posts: { find: async () => ({ id: "1", title: "Hello", authorId: "9" }) }, users: { find: async () => ({ id: "9", name: "Ada" }) }, }; const res = await server.executeOperation( { query: `query ($id: ID!) { post(id: $id) { title author { name } } }`, variables: { id: "1" } }, { contextValue: { db, user: null } } ); assert(res.body.kind === "single"); expect(res.body.singleResult.errors).toBeUndefined(); expect(res.body.singleResult.data?.post).toEqual({ title: "Hello", author: { name: "Ada" } }); });

The assert on body.kind narrows the result type; incremental is the other kind, used by @defer. With GraphQL Yoga, the equivalent is yoga.fetch("/graphql", { method: "POST", body }), which exercises the handler through the Fetch API without a port.

Mocking for Frontend Tests

Frontend components should not depend on a running API. @graphql-tools/mock turns any schema into a server that returns generated values, with overrides for the fields a test cares about:

typescript
import { addMocksToSchema } from "@graphql-tools/mock"; import { makeExecutableSchema } from "@graphql-tools/schema"; const schema = addMocksToSchema({ schema: makeExecutableSchema({ typeDefs }), mocks: { Post: () => ({ title: "Mocked title" }) }, });

For Apollo Client components, MockedProvider from @apollo/client/testing/react replaces ApolloProvider and answers operations with canned responses, one mock per expected request.

Guarding the Schema Itself

Two cheap tests prevent whole categories of production incidents. First, snapshot printSchema(schema) so any change to the public contract shows up in code review. Second, validate every operation document your frontend ships against the schema with validate(schema, parse(document)) from graphql; a removed field then fails CI rather than a user's session. GraphQL Code Generator performs the second check automatically.

Quick Quiz
Question 1 of 2

What does the `contextValue` option of `executeOperation` let a test do?

Key Takeaways

  • Resolvers are plain functions; unit-test them with a hand-built context object.
  • executeOperation (Apollo) or yoga.fetch (Yoga) runs full operations in-process with a controlled contextValue.
  • Keep HTTP-level tests few and focused on middleware, headers and error formatting.
  • Mock schemas with @graphql-tools/mock and Apollo components with MockedProvider.
  • Snapshot printSchema and validate shipped documents to catch breaking changes in CI.

Next lesson: Security: Depth Limiting, Query Complexity and Introspection — protect the server from abusive queries and lock down what production exposes.

Testing Resolvers and Operations - GraphQL | CodeYourCraft | CodeYourCraft