Episode 21 builds a testing strategy for GraphQL: the test pyramid, unit testing resolvers with mocked dependencies, integration testing with a test server and supertest, schema testing and breaking change detection, data mocking with GraphQL Tools, and E2E testing with Cypress.

A complex GraphQL API without testing is a ticking time bomb. Episode 21 builds a comprehensive testing strategy: from the smallest resolver unit tests to full user-flow E2E tests.
We'll learn a GraphQL-specific test pyramid, write unit tests with mocking, integration tests with a real server, schema testing to detect breaking changes, data mocking, and E2E testing with Cypress.
The testing pyramid applies to GraphQL with specific layers:
Install the testing dependencies with npm install -D jest ts-jest @types/jest supertest @apollo/server.
The principle: many unit tests, enough integration tests, few E2E tests. E2E is slowest and most brittle, so reserve it for critical flows.
Unit tests call the resolver directly with fabricated parent, args, and context:
import { resolvers } from "./resolvers";
test("post resolver takes the post from context", async () => {
const mockDb = {
posts: { find: jest.fn().mockResolvedValue({ id: 1, title: "Halo" }) },
};
const result = await resolvers.Query.post(null, { id: "1" }, { db: mockDb });
expect(mockDb.posts.find).toHaveBeenCalledWith("1");
expect(result).toEqual({ id: 1, title: "Halo" });
});Notice: the context is fully mocked, so the resolver is tested in isolation. This makes failures easy to trace — the problem is definitely in the resolver logic, not the database.
For protected resolvers, test all paths: no user, wrong role, and authorized user:
test("deletePost rejects a user without permission", async () => {
const args = { id: "1" };
const ctx = { user: { id: 2, role: "MEMBER" } };
await expect(
resolvers.Mutation.deletePost(null, args, ctx)
).rejects.toThrow("tidak berhak");
});Integration tests run the Apollo server in memory, then send requests like a client:
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 0 } });
test("query posts returns data", async () => {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: "{ posts { id title } }" }),
});
const json = await res.json();
expect(json.errors).toBeUndefined();
expect(json.data.posts).toBeDefined();
});Use port: 0 so the server picks a random port and doesn't conflict between tests. For the database, use an in-memory database (for example better-sqlite3 with Prisma) so tests are fast and hermetic.
If you use Express, supertest is a natural choice for testing endpoints:
import request from "supertest";
import app from "./app";
test("login mutation returns a token", async () => {
const res = await request(app)
.post("/graphql")
.send({ query: `mutation { login(email: "a@b.c", password: "rahasia") { token } }` });
expect(res.body.data.login.token).toBeDefined();
});The schema is a contract, so test schema changes automatically:
npx @graphql-inspector/cli diff schema.graphql new-schema.graphqlGraphQL Inspector compares two schemas and reports breaking changes: removed fields, changed types, added non-null. Run this in CI (episode 32) to block changes that would break clients.
For parallel frontend development, mock every resolver from the schema:
import { addMocksToSchema } from "@graphql-tools/mock";
import { makeExecutableSchema } from "@graphql-tools/schema";
const schema = makeExecutableSchema({ typeDefs });
const mockSchema = addMocksToSchema({ schema });Combine with Faker.js for realistic data. This pattern lets the frontend be developed and tested before the backend is done — we'll revisit it in episode 46.
E2E tests validate real user flows in the browser:
npm install -D cypressdescribe("Login flow", () => {
it("logs in and sees the dashboard", () => {
cy.visit("/login");
cy.get("[data-testid=email]").type("a@b.c");
cy.get("[data-testid=password]").type("rahasia");
cy.get("[data-testid=submit]").click();
cy.contains("Selamat datang").should("be.visible");
});
});To control the GraphQL network in Cypress, use cy.intercept to intercept specific queries and return fixtures — making E2E tests stable without depending on a server.
Key takeaways:
cy.intercept stabilizes E2E tests.In the next episode, episode 22, you'll learn about Apollo Federation for microservices — the supergraph and gateway concepts, Federation 2 features like @shareable and @override, building subgraphs with @key and @extends, setting up Apollo Gateway, and monolith-to-microservices migration strategies. One schema, many services!