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.
| 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.
Resolvers receive everything through arguments, so a test can call one directly (the examples use Vitest; Jest is identical):
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.
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:
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.
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:
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.
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.
What does the `contextValue` option of `executeOperation` let a test do?
executeOperation (Apollo) or yoga.fetch (Yoga) runs full operations in-process with a controlled contextValue.@graphql-tools/mock and Apollo components with MockedProvider.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.