MSW Auto-Mocker
- Handlers:
src/mocks/handlers.tscombines non-schema mocks and auto-generated mocks. - Non-schema mocks: add hand-written handlers in
src/mocks/customHandlers/index.ts. These take precedence over schema-based mocks. - Auto-generated:
src/mocks/mocker.tsreadsswagger.jsonand creates fixtures undersrc/mocks/fixtureson first run.
Usage
- Vitest: tests initialize MSW in
src/mocks/test.setup.ts. Runpnpm test. - Browser (optional): call
startWorker()fromsrc/mocks/browser.tsin your development entry point to mock requests in the browser. - Standalone server (dev):
pnpm mock:serverstarts an HTTP mock server athttp://localhost:9090(configurable viaAPI_BASE_URL). In dev, Next.js rewrites proxy/registry/*to this origin; use relative URLs like/registry/v0.1/serversfrom both client and server code.
Generating fixtures
- To create a new fixture for an endpoint, simply run a Vitest test (or the app in dev) that calls that endpoint. The auto‑mocker will generate
src/mocks/fixtures/<sanitized-path>/<method>.tson first use using schema‑based fake data. - To customize the response, edit the generated TypeScript file. This is preferred over writing a non‑schema mock for simple data tweaks (e.g., replacing lorem ipsum with realistic text). Non‑schema mocks are intended for behavior overrides or endpoints without schema.
Regeneration
- Delete a fixture file to re-generate it on next request.
Failure behavior (always strict)
- If a schema is missing or faker fails, the handler responds 500 and does not write a placeholder.
- Invalid fixtures (including empty
{}when the schema defines properties) respond 500.
Types
- Fixtures use strict types via the
AutoAPIMockwrapper. Generated modules import response types from@api/types.genand pass them as generic parameters toAutoAPIMock<T>for type safety. - Make sure
tsconfig.jsonincludes:"paths": { "@api/*": ["./src/generated/*"] }.
Each fixture is wrapped in AutoAPIMock<T>, which provides test-scoped override capabilities.
Generated fixtures use named exports with a consistent naming convention:
// src/mocks/fixtures/registry_v0_1_servers/get.ts
import type { GetRegistryV01ServersResponse } from "@api/types.gen";
import { AutoAPIMock } from "@mocks";
export const mockedGetRegistryV01Servers = AutoAPIMock<GetRegistryV01ServersResponse>({
servers: [...],
metadata: { count: 15 },
});Use .override() for type-safe response modifications, or .overrideHandler() for full control (errors, network failures):
import { HttpResponse } from "msw";
import { mockedGetRegistryV01Servers } from "@mocks/fixtures/registry_v0_1_servers/get";
// Type-safe data override
mockedGetRegistryV01Servers.override(() => ({
servers: [],
metadata: { count: 0 },
}));
// Modify default data
mockedGetRegistryV01Servers.override((data) => ({
...data,
servers: data.servers?.slice(0, 3),
}));
// Error responses (use overrideHandler)
mockedGetRegistryV01Servers.overrideHandler(() =>
HttpResponse.json({ error: "Server error" }, { status: 500 })
);
// Network error
mockedGetRegistryV01Servers.overrideHandler(() => HttpResponse.error());Overrides are automatically reset before each test via beforeEach() in src/mocks/test.setup.ts.
Define named scenarios in your fixture for commonly used test states:
// src/mocks/fixtures/registry_v0_1_servers/get.ts
import type { GetRegistryV01ServersResponse } from "@api/types.gen";
import { AutoAPIMock } from "@mocks";
import { HttpResponse } from "msw";
export const mockedGetRegistryV01Servers = AutoAPIMock<GetRegistryV01ServersResponse>({
servers: [...],
metadata: { count: 15 },
})
.scenario("empty-servers", (self) =>
self.override(() => ({
servers: [],
metadata: { count: 0 },
})),
)
.scenario("server-error", (self) =>
self.overrideHandler(() =>
HttpResponse.json({ error: "Internal Server Error" }, { status: 500 }),
),
);Then use them in tests:
import { MockScenarios } from "@mocks";
import { mockedGetRegistryV01Servers } from "@mocks/fixtures/registry_v0_1_servers/get";
describe("getServers", () => {
it("returns empty array when API returns no servers", async () => {
mockedGetRegistryV01Servers.activateScenario(MockScenarios.EmptyServers);
const servers = await getServers();
expect(servers).toEqual([]);
});
it("throws on 500 server error", async () => {
mockedGetRegistryV01Servers.activateScenario(MockScenarios.ServerError);
await expect(getServers()).rejects.toBeDefined();
});
});Use activateMockScenario to activate a scenario across all registered mocks at once. This is useful for setting up a consistent state across multiple endpoints, with the option to further customize individual mocks afterwards.
import { activateMockScenario, MockScenarios } from "@mocks";
import { mockedGetRegistryV01Servers } from "@mocks/fixtures/registry_v0_1_servers/get";
describe("error handling", () => {
it("shows error page when all APIs fail", async () => {
// Activate "server-error" on all mocks that define it
// Mocks without this scenario will use their default response
activateMockScenario(MockScenarios.ServerError);
// Test that the app handles the error state correctly
render(<App />);
expect(screen.getByText("Something went wrong")).toBeVisible();
});
it("handles partial failures gracefully", async () => {
// Start with all APIs returning errors
activateMockScenario(MockScenarios.ServerError);
// Then reset specific endpoints to use their default response
mockedGetRegistryV01Servers.reset();
// Now only other endpoints return errors, servers endpoint works
render(<Dashboard />);
expect(screen.getByText("Servers loaded")).toBeVisible();
});
});Scenario names are defined in src/mocks/scenarioNames.ts via the MockScenarios object, which provides autocomplete and JSDoc documentation. Global scenarios are automatically reset before each test via resetAllAutoAPIMocks() in the test setup.