seed-skills/msw-mocking/SKILL.md
Mock Service Worker v2 patterns - http and graphql request handlers, setupServer for Node test runs, setupWorker for the browser, per-test handler overrides, and strict unhandled-request policies.
npx skillsauth add PramodDutta/qaskills MSW API MockingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill makes an AI agent mock HTTP and GraphQL APIs at the network level with Mock Service Worker v2: one set of request handlers shared between Vitest/Jest (via setupServer) and the browser (via setupWorker), per-test overrides with server.use, and an onUnhandledRequest: 'error' policy that catches drift. Trigger it when components or services call fetch/axios in tests, when msw appears in package.json, or when the user is stubbing global.fetch by hand and suffering for it.
vi.mock('./api-client') couples tests to an import path and skips serialization, query strings, and status handling. MSW intercepts actual requests, so the entire client stack (interceptors, retries, parsing) stays under test.handlers.ts is the contract. Define happy-path handlers once; tests, Storybook, and local dev all consume the same array. When the real API changes, you update one file and every consumer notices.500, 422, timeouts) are declared inside the test that needs them via server.use(...), which prepends a one-off override.onUnhandledRequest: 'error' always. Any request without a handler should fail the test loudly. Silent passthrough is how a "unit" test ends up hitting production from CI.server.resetHandlers() in afterEach removes per-test overrides; without it, test order starts to matter and the suite rots.npm install --save-dev msw
# Browser usage only: place the worker script in your static dir
npx msw init public/ --save
// src/mocks/handlers.ts
import { http, HttpResponse, delay } from 'msw';
export interface User {
id: string;
name: string;
role: 'admin' | 'member';
}
export const handlers = [
http.get('https://api.example.com/users/:id', ({ params }) => {
return HttpResponse.json<User>({
id: String(params.id),
name: 'Ada Lovelace',
role: 'admin',
});
}),
http.get('https://api.example.com/orders', ({ request }) => {
const url = new URL(request.url);
const page = Number(url.searchParams.get('page') ?? '1');
return HttpResponse.json({
items: [{ id: 'ord_1', total: 4999 }],
page,
totalPages: 3,
});
}),
http.post('https://api.example.com/orders', async ({ request }) => {
const body = (await request.json()) as { sku?: string; qty?: number };
if (!body.sku) {
return HttpResponse.json({ error: 'sku is required' }, { status: 422 });
}
await delay(50); // simulate realistic latency
return HttpResponse.json({ orderId: 'ord_2', ...body }, { status: 201 });
}),
];
// src/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// vitest.setup.ts (register via test.setupFiles in vitest.config.ts)
import { beforeAll, afterEach, afterAll } from 'vitest';
import { server } from './src/mocks/server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// src/components/UserProfile.test.tsx
import { render, screen } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { server } from '../mocks/server';
import { UserProfile } from './UserProfile';
it('renders the user fetched from the API', async () => {
render(<UserProfile id="42" />);
expect(await screen.findByRole('heading', { name: 'Ada Lovelace' })).toBeInTheDocument();
});
it('shows an error banner when the API is down', async () => {
// One-off override; resetHandlers() in afterEach removes it
server.use(
http.get('https://api.example.com/users/:id', () =>
HttpResponse.json({ message: 'internal error' }, { status: 500 }),
),
);
render(<UserProfile id="42" />);
expect(await screen.findByRole('alert')).toHaveTextContent('Could not load profile');
});
it('handles a network-level failure distinctly from a 500', async () => {
server.use(
http.get('https://api.example.com/users/:id', () => HttpResponse.error()),
);
render(<UserProfile id="42" />);
expect(await screen.findByRole('alert')).toHaveTextContent('Check your connection');
});
// src/api/orders.test.ts
import { http, HttpResponse } from 'msw';
import { server } from '../mocks/server';
import { createOrder } from './orders';
it('sends the auth header and JSON body the API expects', async () => {
let captured: { auth: string | null; body: unknown } | undefined;
server.use(
http.post('https://api.example.com/orders', async ({ request }) => {
captured = {
auth: request.headers.get('authorization'),
body: await request.json(),
};
return HttpResponse.json({ orderId: 'ord_9' }, { status: 201 });
}),
);
await createOrder({ sku: 'SKU-1', qty: 2 }, { token: 'jwt-abc' });
expect(captured?.auth).toBe('Bearer jwt-abc');
expect(captured?.body).toEqual({ sku: 'SKU-1', qty: 2 });
});
// src/mocks/graphql-handlers.ts
import { graphql, HttpResponse } from 'msw';
export const gqlHandlers = [
graphql.query('GetCart', ({ variables }) => {
return HttpResponse.json({
data: {
cart: { id: variables.cartId, items: [{ sku: 'SKU-1', qty: 1 }] },
},
});
}),
graphql.mutation('AddToCart', () => {
return HttpResponse.json({
errors: [{ message: 'Out of stock', extensions: { code: 'OUT_OF_STOCK' } }],
});
}),
];
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// src/main.tsx -- enable mocking only in development
async function enableMocking(): Promise<void> {
if (!import.meta.env.DEV) return;
const { worker } = await import('./mocks/browser');
await worker.start({ onUnhandledRequest: 'bypass' });
}
enableMocking().then(() => {
createRoot(document.getElementById('root')!).render(<App />);
});
HttpResponse.json<User>(...)) so mock drift becomes a compile error when the app's types change.delay() in handlers that back loading-state tests; a 0ms response can resolve before React renders the spinner you are asserting on.:id) and URL query parsing in handlers instead of one handler per exact URL; fewer handlers, broader coverage.searchParams so the same handler serves page 1 and page 7 tests.setupFilesAfterEach/setupFilesAfterEach-equivalent (setupFilesAfterEach is Vitest; Jest uses setupFilesAfterEach? use setupFilesAfterEach carefully) - concretely: setupFiles: ['<rootDir>/jest.setup.ts'] with the same listen/reset/close trio.handlers.ts.global.fetch = vi.fn() and hand-crafting Response objects: brittle, skips URL matching, and dies the day you switch to axios.onUnhandledRequest: 'bypass' in tests: unmocked calls silently reach real services, making tests slow, flaky, and occasionally destructive.server.resetHandlers() in afterEach, then debugging why a 500 override leaks into the next twelve tests.handlers.ts and override locally.fetch, axios, or API client modules by hand, or a component test suite needs network responses.msw is in package.json, mockServiceWorker.js is in public/, or setupServer/setupWorker appears in the codebase.development
Generate test data from the schemas you already have. Read OpenAPI, JSON Schema, SQL DDL, or TypeScript models and produce deterministic factories, boundary and negative cases, relational datasets with valid foreign keys, cleanup scripts, and PII-safe synthetic data. Production records never leave the machine.
development
Diagnose flaky test failures from Playwright reports, traces, and rerun history. Classify each failure as product, test, environment, data, or unknown with cited evidence and a proposed fix. Never auto-modifies code without opt-in.
tools
Test LLM, RAG, MCP, and agentic systems end to end. Build golden datasets, run deterministic checks and LLM judges, score retrieval, probe prompt injection, verify tool use, and gate CI on thresholds. Orchestrates DeepEval, Ragas, promptfoo, and Langfuse.
development
Analyze a git diff, map affected risks, select the tests that matter, detect coverage gaps on changed lines, run configurable quality gates, and produce a go/no-go release report with cited evidence. Recommends only; never merges or deploys.