Using MSW to mock GraphQL endpoints locally
Establishing a deterministic local testing environment requires strict isolation from live backend dependencies. By intercepting GraphQL operations at the network layer, teams eliminate flaky test suites caused by external service volatility. This architecture aligns local development with CI/CD pipeline stability objectives, enforcing fast resolution times and strict schema validation before code reaches staging. The following patterns provide a production-ready blueprint for intercepting, mocking, and validating GraphQL traffic using Mock Service Worker (MSW) v2.
Root Cause Analysis: Why Unmocked GraphQL Breaks Local & CI Environments
Uncontrolled GraphQL dependencies introduce three primary failure vectors that degrade test reliability: schema drift, network latency variance, and race conditions during parallel test execution. When test suites hit live endpoints, implicit type coercion failures occur as soon as mock payloads diverge from the production GraphQL schema. Unhandled resolver timeouts and cross-test cache poisoning further compound these issues, making External Service Simulation unreliable in modern CI pipelines. Without strict interception, test runners inherit backend state, leading to non-deterministic outcomes that block merges and waste engineering cycles.
Reproducible MSW Setup for GraphQL Interception
Achieving deterministic mocking requires exact server initialization with GraphQL-specific handlers. MSW v2 uses a resolver-based API — the old (req, res, ctx) signature was removed in v2. The following configuration guarantees consistent behavior in Node test environments (Vitest, Jest).
1. Install MSW v2
npm install msw --save-dev
2. Define GraphQL handlers (MSW v2 API)
// src/mocks/handlers.ts
import { graphql, HttpResponse } from 'msw';
export const handlers = [
graphql.query('GetUser', ({ variables }) => {
// Strict request matching prevents fallback to live network
return HttpResponse.json({
data: {
user: {
id: variables.id ?? '1',
name: 'Test User',
__typename: 'User',
},
},
});
}),
];
3. Bind server lifecycle to the test runner
For Node-based runners (Vitest, Jest), use setupServer — not setupWorker. setupWorker is the browser Service Worker variant and does not work in Node.
// src/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// vitest.setup.ts (or jest.setup.ts)
import { server } from './src/mocks/server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
The onUnhandledRequest: 'error' option is critical: it turns unmocked GraphQL operations into hard failures rather than silent passthroughs, giving you an early warning when a new query is added without a corresponding handler.
Pipeline Stability: Deterministic Response Generation & Cache Control
Parallel test execution frequently triggers cache poisoning and stale resolver states. Integrating with Advanced Mocking & Service Isolation Patterns requires strict request matching and explicit cache invalidation to prevent cross-test pollution.
Mitigation Protocol:
- Validate Variables Early: Inspect
variablesinside the resolver before returning a response. Return a GraphQL error for malformed payloads. - Avoid Implicit Latency: Never rely on backend latency in tests. Use
HttpResponse.json(...)directly for synchronous responses. - Reset Client Caches: Clear Apollo/Relay caches in
afterEachhooks to guarantee state isolation between tests. - Serialize Stateful Mutations: When testing GraphQL mutations that alter shared mock state, run those suites sequentially (
--pool=forks --singleForkor--sequence.concurrent=falsein Vitest).
// afterEach hook example for Apollo Client cache reset
afterEach(() => {
client.cache.reset();
});
Exact Mitigation Steps for Edge Cases & Type Safety
GraphQL mocking introduces unique edge cases that require precise architectural patterns to resolve.
Type-Safe Payload Generation
Integrate graphql-codegen to generate TypeScript interfaces directly from your schema. Use these types to construct mock payloads, eliminating implicit type coercion failures at compile time.
Deterministic Pagination
Mock pageInfo and edges arrays explicitly to prevent infinite scroll or cursor-based logic from hanging:
// src/mocks/handlers.ts
import { graphql, HttpResponse } from 'msw';
graphql.query('GetItems', () => {
return HttpResponse.json({
data: {
items: {
pageInfo: { hasNextPage: false, endCursor: 'cursor-1' },
edges: [{ node: { id: '1', title: 'Item A' }, cursor: 'cursor-1' }],
},
},
});
});
Network Error Simulation
Simulate DNS failures or dropped connections using MSW v2’s HttpResponse.error():
import { graphql, HttpResponse } from 'msw';
graphql.query('FetchData', () => {
return HttpResponse.error(); // Simulates a network-level failure (ECONNREFUSED)
});
GraphQL Error Responses
Return application-level GraphQL errors (status 200 with an errors array) to test client-side error boundaries:
graphql.query('FetchDashboard', () => {
return HttpResponse.json({
data: null,
errors: [
{
message: 'Internal server error',
path: ['dashboard'],
extensions: { code: 'INTERNAL_SERVER_ERROR' },
},
],
});
});
Validation & Teardown Protocol
Clean state management between test suites prevents memory leaks and ensures deterministic execution across CI runs.
1. Global Teardown
Always invoke server.close() in afterAll. This releases internal Node.js interceptors and closes any underlying connections MSW holds.
2. Catch Unhandled Resolver Exceptions
Convert console errors into hard test failures to surface unexpected GraphQL errors early:
beforeEach(() => {
vi.spyOn(console, 'error').mockImplementation((...args) => {
throw new Error(`Unexpected console.error: ${args.join(' ')}`);
});
});
afterEach(() => {
vi.restoreAllMocks();
});
3. Node Environment Integration
For all integration tests running in Node (Vitest, Jest), always use setupServer from msw/node. The setupWorker export is exclusively for browser Service Worker environments and will throw at import time in Node.
4. Enforce Full Handler Coverage
Use onUnhandledRequest: 'error' in server.listen() to fail any test that issues an unmocked GraphQL operation. This forces developers to explicitly define mock contracts rather than relying on implicit network fallbacks.
By enforcing these patterns, teams achieve zero-flake local GraphQL testing, deterministic CI pipelines, and strict contract validation between frontend and backend services.