Testing GraphQL Queries With Mock Service Worker
Testing GraphQL APIs requires intercepting POST requests to your GraphQL endpoint and matching on the operation name and variables, rather than just the URL. MSW provides a graphql module with query() and mutation() handlers that extract the operation name and variables from the request, letting you return typed responses that match your schema.
While working on a content-discovery app that used Apollo Client with multiple GraphQL queries, I found that traditional HTTP mocking forced us to match on raw JSON request bodies, making tests brittle. MSW's graphql handlers simplified this dramatically by letting us reference operation names like GetArticles directly.
Setting Up GraphQL Handlers
GraphQL handlers use the same server setup but are defined with graphql instead of http. The endpoint is the URL where your GraphQL server listens (typically /graphql).
import { graphql, HttpResponse } from "msw";
export const graphqlHandlers = [
graphql.query("GetUser", ({ variables }) => {
return HttpResponse.json({
data: {
user: {
id: variables.id,
name: "Alice Chen",
email: "[email protected]",
},
},
});
}),
graphql.mutation("CreatePost", ({ variables }) => {
return HttpResponse.json({
data: {
createPost: {
id: "post-123",
title: variables.title,
content: variables.content,
published: true,
},
},
});
}),
];
The operation name (e.g., GetUser) must match the GraphQL query name exactly. The variables object contains the variables passed to the query. Return an object with a data key matching your GraphQL schema.
Matching GraphQL Operations by Name and Variables
GraphQL queries often accept variables that affect the response shape. MSW allows you to return different responses based on variable values, enabling rich test scenarios.
graphql.query("GetArticles", ({ variables }) => {
// Filter by category if provided
const articles =
variables.category === "tutorials"
? [
{
id: "1",
title: "React Hooks Guide",
category: "tutorials",
views: 1200,
},
{
id: "2",
title: "GraphQL Best Practices",
category: "tutorials",
views: 890,
},
]
: [
{
id: "3",
title: "News Roundup",
category: "news",
views: 5600,
},
];
return HttpResponse.json({
data: {
articles,
},
});
});
This pattern lets you test that your component correctly passes variables to GraphQL and handles different result shapes. Pagination testing becomes straightforward: vary the limit and offset variables to return different subsets.
Returning GraphQL Errors
Real GraphQL APIs return errors in a specific format: an errors array alongside (or instead of) the data key. Your React component should handle these gracefully.
graphql.query("GetUser", ({ variables }) => {
// Simulate a not-found error
if (variables.id === "999") {
return HttpResponse.json({
errors: [
{
message: "User not found",
extensions: {
code: "NOT_FOUND",
},
},
],
});
}
// Simulate an authentication error
if (variables.id === "forbidden") {
return HttpResponse.json({
errors: [
{
message: "Unauthorized",
extensions: {
code: "UNAUTHENTICATED",
},
},
],
});
}
return HttpResponse.json({
data: {
user: {
id: variables.id,
name: "User Name",
},
},
});
});
By returning errors in the GraphQL error format, you can test that your component correctly identifies and displays API errors without confusing them with network failures (where no response is received at all).
Testing Mutations and Side Effects
Mutations modify data on the server. Your handlers can track mutations to verify your component submitted the correct data, or return updated state to verify the UI re-renders.
let capturedPostData = null;
export const graphqlHandlers = [
graphql.mutation("CreatePost", ({ variables }) => {
capturedPostData = variables;
// Validate required fields
if (!variables.title || variables.title.length < 3) {
return HttpResponse.json({
errors: [
{
message: "Title is required and must be at least 3 characters",
field: "title",
},
],
});
}
return HttpResponse.json({
data: {
createPost: {
id: `post-${Date.now()}`,
title: variables.title,
content: variables.content,
authorId: variables.authorId,
publishedAt: new Date().toISOString(),
},
},
});
}),
];
// In your test
test("submits post creation with correct variables", async () => {
render(<CreatePostForm />);
await userEvent.type(screen.getByLabelText(/title/i), "New Post");
await userEvent.type(screen.getByLabelText(/content/i), "Post body...");
await userEvent.click(screen.getByRole("button", { name: /publish/i }));
await waitFor(() => {
expect(capturedPostData).toEqual({
title: "New Post",
content: "Post body...",
authorId: expect.any(String),
});
});
});
Capturing the mutation variables lets you verify that your component submitted the correct data, catching bugs where required fields are missing or incorrectly transformed.
Using MSW With Apollo Client
Apollo Client is the most popular GraphQL library for React. It works seamlessly with MSW handlers because Apollo uses fetch under the hood.
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
import { gql } from "@apollo/client";
const client = new ApolloClient({
link: new HttpLink({ uri: "/graphql" }),
cache: new InMemoryCache(),
});
// GraphQL query
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
// Test using Apollo and MSW
test("Apollo client queries with MSW handlers", async () => {
const { result } = renderHook(
() => useQuery(GET_USER, { variables: { id: "1" } }),
{ wrapper: ApolloProvider, client }
);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(result.current.data.user.name).toBe("Alice Chen");
});
});
Apollo Client automatically extracts the operation name from your GraphQL query string, so MSW's graphql.query() handlers will match without additional configuration. This makes it effortless to test Apollo-based components with MSW.
Handling Nested Fields and Fragments
GraphQL queries often request nested fields. Your handler can return a response shape that matches the query structure.
graphql.query("GetUserWithPosts", ({ variables }) => {
return HttpResponse.json({
data: {
user: {
id: variables.userId,
name: "Alice Chen",
email: "[email protected]",
posts: [
{
id: "post-1",
title: "First Post",
likes: 42,
comments: [
{ id: "c-1", author: "Bob", text: "Great post!" },
],
},
],
},
},
});
});
MSW does not validate that your returned data matches the query structure, so it is your responsibility to return a response shape your component expects. This flexibility allows you to test how components handle missing fields or unexpected nesting.
Key Takeaways
- GraphQL handlers use
graphql.query()andgraphql.mutation()instead ofhttp.get()/http.post(), matching on operation name instead of URL. - Variables passed to GraphQL operations are accessible in the handler, enabling response variations based on input.
- GraphQL errors are returned in a specific format with an
errorsarray; handlers can return bothdataanderrorstogether. - Apollo Client works seamlessly with MSW because it uses
fetchinternally; no additional setup is required. - Capturing mutation variables in handlers lets you verify that your component submitted the correct GraphQL data.
Frequently Asked Questions
Does MSW support GraphQL subscriptions?
MSW currently supports query and mutation handlers via HTTP. For subscriptions (WebSocket-based), you need a separate WebSocket mock library or a real GraphQL subscription server in your test environment.
How do I test Apollo Client's cache behavior with MSW?
MSW only affects network responses. Apollo's cache operates on top of those responses. To test caching, trigger two queries with identical variables and verify Apollo does not make a second network request. MSW will not intercept the second request because Apollo serves it from cache.
Can I mock queries to different GraphQL endpoints?
Yes. Each graphql.query() handler can be for a different endpoint URL by specifying the full URL: graphql.query("GetUser", { endpoint: "https://api2.example.com/graphql" }, ...)
What if my GraphQL endpoint is not /graphql?
Specify the full URL in your handlers: graphql.query("GetUser", { endpoint: "/custom-gql-path" }, ...)