Skip to content

feat(openapi-msw): emit per-schema fixture builders, not just a full handler set #426

Description

@benjamineckstein

Package

@codewithagents/openapi-msw

OpenAPI spec version

3.1

Problem

openapi-msw today generates the whole mock surface: one handler per operation, with a seeded Faker body shaped from the 2xx response schema. That is the right shape when you want a mock layer, for a browser worker or a standalone Node mock server.

It does not fit the other common setup, where a component test is deliberately not backed by a mock layer and instead declares the one or two handlers it cares about inline, so that what the test asserts on is visible in the test:

const server = setupServer(
  http.get('/api/v1/pets', () => HttpResponse.json({ items: [PET_FIXTURE], page: { hasMore: false } })),
)

The handler is cheap. The fixture is not. PET_FIXTURE has to be a complete, valid Pet, so every required field gets hand-written even though the test only cares about one of them. The type import from the generated models means tsc catches a shape change, which is genuinely valuable, but it does not write the value, so every required field added to a schema is a mechanical edit in every test that built one of these by hand.

Measured on a private codebase that uses this suite (a mid-sized app, ~240 web test files):

  • 92 files stand up an msw server, declaring 681 handlers between them.
  • The handler bodies themselves are only ~9% of those files' lines, averaging 5.2 lines each.
  • Roughly half the remaining lines are fixture and arrange data.
  • One frequently-needed model had its fixture builder hand-written in 39 separate files, in three different signatures, because there was nowhere shared to put it.

A hand-written fixture library solves it locally but reintroduces the problem the generator exists to remove: it is a second description of the schema, and it drifts silently in exactly the direction tsc cannot see (a value that is still the right type but no longer satisfies a format, an enum, or a minLength).

Proposed solution

Emit per-schema fixture builders alongside the handlers, so a test can state only the fields it cares about and get a complete, valid object for the rest:

import { aPet, anOrder } from './mocks/fixtures.js'

const PET = aPet({ name: 'Rex', status: 'available' })
// every other required field filled from the schema, deterministically

http.get('/api/v1/pets', () => HttpResponse.json({ items: [aPet(), aPet({ id: 7 })] }))

Shape, roughly:

export function aPet(overrides?: Partial<Pet>): Pet

The important part is that the engine for this already exists in this package. Generating a schema-shaped Faker body honouring string formats, enums, arrays and nested objects is exactly what the handler generator does. This asks for that same machinery exposed at a smaller granularity: per named schema component rather than only per operation response.

Properties that would make it usable in a test suite:

  • Deterministic by default. The existing seed behaviour is right, but a test needs repeatability per call, not just per generation run: aPet() called twice in one test should not silently produce two different ids unless asked to. Some explicit control (a per-builder counter, or a seed argument) matters more here than in a handler set.
  • Overrides are shallow-merged and type-checked, so aPet({ status: 'nope' }) fails tsc against the enum.
  • Required-only by default, with optional fields omitted rather than filled, so a test asserting "this optional field is absent" is expressible. Possibly aPet.full() for the opposite.
  • Opt-in, e.g. a fixtures: true config flag writing fixtures.ts next to handlers.ts. Nobody generating a mock worker should pay for output they do not import.

Alternatives considered

  • Adopt the generated handlers.ts as-is. Rejected for this style of test suite: a generated handler set is a mock layer, and the value of the inline-handler style is precisely that a reader sees which responses a test depends on. It also cannot express the failure cases these suites lean on heavily (in the codebase above, 119 of 681 handlers return a non-2xx deliberately, and 48% of the files simulate at least one failure), since the generated handler returns the 2xx body.
  • A hand-written fixture library in the consuming repo. Works, and is what most teams end up with, but it is a second source of truth for the schema. It drifts in the way tsc cannot catch, which is the class of problem this whole suite is built to remove.
  • Derive fixtures from the generated Zod schemas at runtime (something like a zod-mock pass over schemas.ts). Plausible, and it keeps one source of truth, but it puts a generator dependency into the test runtime and gives up the deterministic-output-you-own property this suite is otherwise strict about.

Additional context

Two smaller observations from adopting the suite, both adjacent and neither worth its own issue:

  1. @codewithagents/openapi-react-query already generates test-utils.ts with createTestQueryClient() and createWrapper(). In the codebase above these were imported by zero files, while 79 hand-wrote a QueryClient with byte-identical options and 86 declared their own wrapper. That is a discoverability problem rather than a code one: nothing in the quick start points at the generated test utilities, so people rebuild them. A line in the README, or a mention in the generated file's header comment, would likely be enough.

  2. If fixtures do get generated, the same README should probably say plainly which of the two styles each output is for (handlers.ts for a mock layer, fixtures.ts for inline per-test handlers). They look interchangeable from the outside and are not.

Happy to prototype this if the direction seems right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions