GraphQL Client Mocking Strategies: MSW, graphql-tools Mock Schema, and Beyond
When testing GraphQL client code, you have multiple layers where you can inject mocks: at the network level (MSW), at the schema level (graphql-tools), at the client level (Apollo MockedProvider, urql exchanges), or at the component level. Each approach has tradeoffs. This guide compares them and shows when to use each.
The Four Layers of GraphQL Mocking
Component
↓
GraphQL Client (Apollo, urql, Relay)
↓
HTTP Transport (fetch, WebSocket)
↓
ServerYou can intercept at any layer:
| Layer | Tool | Tests | Speed | Realism |
|---|---|---|---|---|
| Component | jest.mock() | Isolated component logic | Fastest | Low |
| Client | MockedProvider, urql exchanges, MockEnvironment | Client-component integration | Fast | Medium |
| Network | MSW | Full client behavior | Medium | High |
| Schema | graphql-tools | Schema contract | Medium | High |
The right choice depends on what you're testing.
MSW for Network-Level Mocking
Mock Service Worker intercepts actual fetch calls. Your GraphQL client behaves exactly as it would in production — the only difference is what's on the other end of the network call.
Setup
npm install --save-dev msw
npx msw init public/ --save// test-setup/msw-handlers.js
import { graphql, HttpResponse } from 'msw';
export const handlers = [
graphql.query('GetUser', ({ variables }) => {
if (variables.id === '1') {
return HttpResponse.json({
data: {
user: {
id: '1',
name: 'Jane Smith',
email: 'jane@example.com',
role: 'ADMIN',
},
},
});
}
return HttpResponse.json({
errors: [{ message: 'User not found' }],
});
}),
graphql.mutation('UpdateUser', ({ variables }) => {
return HttpResponse.json({
data: {
updateUser: {
id: variables.id,
name: variables.input.name,
email: variables.input.email,
},
},
});
}),
graphql.subscription('OrderStatus', () => {
// Subscriptions need a different approach (see below)
}),
];// test-setup/msw-server.js
import { setupServer } from 'msw/node';
import { handlers } from './msw-handlers';
export const server = setupServer(...handlers);
// In jest setup file:
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());Using MSW in Tests
import { server } from '../test-setup/msw-server';
import { graphql, HttpResponse } from 'msw';
test('displays user profile', async () => {
// Uses default handler — works for all clients (Apollo, urql, Relay, fetch)
render(<UserProfile userId="1" />);
await waitFor(() => {
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
});
});
test('shows error for unknown user', async () => {
// Override handler for this specific test
server.use(
graphql.query('GetUser', () => {
return HttpResponse.json({
errors: [{ message: 'Unauthorized' }],
});
})
);
render(<UserProfile userId="1" />);
await waitFor(() => {
expect(screen.getByText('Unauthorized')).toBeInTheDocument();
});
});MSW for Delayed Responses
Test loading states and timeouts:
import { delay } from 'msw';
test('shows loading skeleton during slow response', async () => {
server.use(
graphql.query('GetUser', async () => {
await delay(1000); // 1 second delay
return HttpResponse.json({
data: { user: { id: '1', name: 'Jane', email: 'j@e.com', role: 'USER' } },
});
})
);
render(<UserProfile userId="1" />);
expect(screen.getByTestId('skeleton-loader')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Jane')).toBeInTheDocument();
}, { timeout: 2000 });
});graphql-tools Schema Mocking
graphql-tools lets you create a fully mock GraphQL server from your schema definition. This is ideal for testing against the real schema without a backend:
npm install --save-dev @graphql-tools/mock @graphql-tools/schema graphqlBasic Schema Mock Server
import { addMocksToSchema } from '@graphql-tools/mock';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { graphql } from 'graphql';
const typeDefs = `
type User {
id: ID!
name: String!
email: String!
role: UserRole!
posts: [Post!]!
}
enum UserRole { ADMIN USER GUEST }
type Post {
id: ID!
title: String!
publishedAt: String
}
type Query {
user(id: ID!): User
users(limit: Int): [User!]!
}
type Mutation {
updateUser(id: ID!, name: String!): User
}
`;
const schema = makeExecutableSchema({ typeDefs });
// Add default mocks — auto-generates values for all types
const mockedSchema = addMocksToSchema({ schema });Customizing Mock Resolvers
Override specific types and fields:
const customMocks = {
User: () => ({
id: () => 'user-1',
name: () => 'Generated User',
email: () => 'generated@example.com',
role: () => 'ADMIN',
}),
Post: () => ({
id: () => `post-${Math.random().toString(36).slice(2)}`,
title: () => 'Generated Post Title',
publishedAt: () => new Date().toISOString(),
}),
// Scalars
ID: () => `id-${Math.random().toString(36).slice(2)}`,
String: () => 'Mock String',
};
const mockedSchema = addMocksToSchema({ schema, mocks: customMocks });
// Use in test
const result = await graphql({
schema: mockedSchema,
source: `query { user(id: "1") { id name email role } }`,
});
console.log(result.data.user);
// { id: 'user-1', name: 'Generated User', email: 'generated@example.com', role: 'ADMIN' }Serving Mock Schema with MSW
Combine graphql-tools with MSW for a powerful pattern — your handlers execute against the mock schema, ensuring type correctness:
import { graphql as graphqlTools } from 'graphql';
import { graphql as mswGraphql, HttpResponse } from 'msw';
function createSchemaHandler(operationName, schema) {
return mswGraphql.all(async ({ query, variables }) => {
const result = await graphqlTools({
schema,
source: query,
variableValues: variables,
});
return HttpResponse.json(result);
});
}
const server = setupServer(createSchemaHandler('*', mockedSchema));Comparing the Approaches
When to Use MSW
- Testing a component with any GraphQL client without client-specific setup
- Integration tests where you want realistic HTTP behavior
- Testing error responses, timeouts, and network failures
- Shared mock data across multiple tests or test files
- Testing the Apollo/urql/Relay error handling, retries, and caching at the HTTP level
// Perfect MSW use case: cross-client integration test
test('Apollo and urql both handle the same response correctly', async () => {
// MSW handler applies to both clients
const apolloResult = render(
<ApolloProvider client={apolloClient}>
<UserProfile userId="1" />
</ApolloProvider>
);
await waitFor(() => screen.getByText('Jane Smith'));
apolloResult.unmount();
const urqlResult = render(
<Provider value={urqlClient}>
<UserProfile userId="1" />
</Provider>
);
await waitFor(() => screen.getByText('Jane Smith'));
});When to Use Client-Level Mocks (MockedProvider, mock exchanges)
- Unit tests for component-specific behavior (loading states, error display, optimistic UI)
- Testing Apollo cache interactions
- Testing Relay fragment composition
- Tests that need to simulate client-specific features (Apollo reactive variables, Relay store updates)
// Perfect Apollo MockedProvider use case: testing optimistic UI
test('shows optimistic like count before server confirms', async () => {
let resolve;
const mocks = [{
request: { query: LIKE_POST, variables: { postId: '1' } },
result: () => new Promise(r => { resolve = r; }),
}];
// Only possible with client-level mocking — MSW can't pause response mid-test easily
render(<MockedProvider mocks={mocks}><LikeButton postId="1" /></MockedProvider>);
await user.click(screen.getByRole('button'));
// Assert optimistic state before server responds
expect(screen.getByText('43 likes')).toBeInTheDocument();
resolve({ data: { likePost: { likeCount: 43 } } });
});When to Use graphql-tools Schema Mocking
- Testing query compatibility against your actual schema
- Catching schema breaking changes in CI
- Generating large, realistic datasets for complex components
- Contract testing between frontend and backend
// graphql-tools: perfect for schema contract testing
test('all queries in the codebase are compatible with the schema', async () => {
const queries = [GET_USER, GET_PRODUCTS, GET_ORDER]; // all your app's queries
for (const query of queries) {
const result = await graphql({ schema: mockedSchema, source: print(query) });
expect(result.errors).toBeUndefined();
}
});Practical Patterns
Shared Mock Factory
Create a central mock factory that all tests can use:
// test-utils/graphql-mocks.js
export const mockUser = (overrides = {}) => ({
id: 'user-1',
name: 'Test User',
email: 'test@example.com',
role: 'USER',
...overrides,
});
export const mockProduct = (overrides = {}) => ({
id: 'product-1',
name: 'Test Product',
price: 99.99,
inStock: true,
...overrides,
});
// MSW handler using factory
export const defaultHandlers = [
graphql.query('GetUser', ({ variables }) =>
HttpResponse.json({ data: { user: mockUser({ id: variables.id }) } })
),
graphql.query('GetProduct', ({ variables }) =>
HttpResponse.json({ data: { product: mockProduct({ id: variables.id }) } })
),
];Testing Partial Responses
GraphQL allows partial success — some fields succeed while others return errors:
test('renders partial data with inline errors', async () => {
server.use(
graphql.query('GetDashboard', () =>
HttpResponse.json({
data: {
user: { id: '1', name: 'Jane' },
analytics: null, // This field failed
},
errors: [
{
message: 'Analytics service unavailable',
path: ['analytics'],
},
],
})
)
);
render(<Dashboard userId="1" />);
await waitFor(() => {
expect(screen.getByText('Jane')).toBeInTheDocument();
expect(screen.getByText('Analytics unavailable')).toBeInTheDocument();
});
});E2E Validation with HelpMeTest
Mock-based tests are fast, but they can't catch server-side schema changes, production API bugs, or client-server version mismatches. HelpMeTest's E2E tests validate your GraphQL integration in real browsers against real servers:
Navigate to https://your-app.com/dashboard
Verify user name appears in the header
Verify analytics panel loads within 3 seconds
Verify product list loads and shows at least 5 items
Filter products by category
Verify filtered results are correct
Update profile name via the edit form
Verify name change persists after page refreshThese tests run continuously, alerting you when your GraphQL server changes break the client.
Summary
Choose your GraphQL mocking strategy based on what you're testing:
- MSW — best for cross-client integration tests, realistic HTTP behavior, and shared mocks
- graphql-tools schema mocking — best for schema contract testing and type-correct data generation
- Client mocks (MockedProvider, mock exchanges, MockEnvironment) — best for component unit tests and client-specific features
- Combination — use MSW for most tests, client-level mocks when you need to control client internals
In all cases, complement your mock tests with E2E monitoring via HelpMeTest to catch production GraphQL regressions that mocks can never detect.