Skip to main content
CodeAlchemy
GraphQL

Mocking GraphQL schemas: a practical guide to fast, realistic data

CodeAlchemy Team··7 min read

Source: graphql-tools mocking guide

GraphQL

Mocking GraphQL is one of those things every team eventually does, usually badly the first time. You write fixtures by hand, they go stale, the schema changes, the fixtures fail to typecheck, and someone deletes them out of frustration. This post is about the alternative: schema-driven mocking, where the SDL itself is the source of truth and the mock data is generated from it.

If your SDL changes, your mocks should change automatically. That's the test.

Why mock at all?

There are three real reasons to mock a GraphQL schema:

1. Frontend development before the backend exists. A common pattern when teams ship API contracts first: agree on the SDL, mock against it on the frontend, fill in the resolvers in parallel. 2. Tests that need realistic data without a server. Unit tests, component tests, and Storybook stories all benefit from believable users, posts, and timestamps without spinning up a database. 3. Demos and design reviews. "Let me show you what this list looks like with twenty items" is a one-line config change instead of an afternoon of seeding.

The wrong reason is "we don't trust the backend yet, so we'll keep mocks forever." Mocks are scaffolding; treat them like scaffolding.

Schema-driven vs hand-authored fixtures

A hand-authored fixture looks like this:

[
  { "id": "1", "name": "Alice", "email": "[email protected]" },
  { "id": "2", "name": "Bob", "email": "[email protected]" }
]

Add a field to the schema, every fixture goes stale. Remove a field, your TypeScript starts complaining. Worse, the values are unrealistic — Alice and Bob show up in screenshots, and three sprints later someone is still demoing with [email protected].

A schema-driven mock looks like this:

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

You feed the SDL to a generator. It walks the type, sees String fields named name and email, and uses Faker.js to produce realistic values: Maya Lindgren, [email protected]. You get one fake User per call, ten if you ask for ten, fifty if you ask for fifty. Add a field to the SDL, the next generation includes it.

Heuristics that make mocks feel real

The difference between "looks like staging data" and "looks like Lorem Ipsum" is in the heuristics. Two heuristics carry most of the weight:

  • Field-name → Faker generator mapping. A field called email shouldn't be a random string — it should be faker.internet.email(). A field called firstName should be faker.person.firstName(). A field called city is faker.location.city(). The mapping table is small (a few dozen names cover 80% of cases), but it's the difference between mocks that read like real users and mocks that look like generated noise.
  • Format-aware ID generation. Fields of type ID typically want UUIDs. Some teams use sequential integers; the generator can match either depending on the format hint or context.

The GraphQL → TypeScript tool's mock generator implements both heuristics. Set the locale to ru and you get Cyrillic names; set it to fr and you get French addresses. Set the count to fifty and you get fifty mock objects per type.

Depth: where mocks get expensive

Schema-driven mocks have one tricky parameter: depth. Consider:

type User {
  id: ID!
  posts: [Post!]!
}

type Post {
  id: ID!
  author: User!
  comments: [Comment!]!
}

type Comment {
  id: ID!
  post: Post!
  author: User!
}

Naively walking this generates infinite data. User has posts, each Post has an author that's a User, which has posts, ad infinitum. Even without circular references, depth-3 mocks for a richly interconnected schema produce thousands of objects per top-level entity.

The pragmatic fix is depth limiting. The mock generator walks the schema down to a configurable depth (typically 1–3) and bottoms out at null or [] after that.

  • Depth 1 is enough for screenshots: top-level fields filled, relations as empty arrays.
  • Depth 2 is enough for most component tests: one level of relations expanded.
  • Depth 3 is enough for nested list rendering: a User has Posts, each Post has Comments, each Comment has an author and stops.
  • Depth 4+ is rarely needed and exponentially expensive.

Set this consciously. A test that needs User.posts[0].comments[0].author.name requires depth 4, but most don't.

Workflow patterns

Three patterns cover most of what teams do with schema-driven mocks:

1. Generate once, commit. Run the generator, save mocks.json in the repo, import it from tests and Storybook stories. Regenerate when the SDL changes — the diff is the changelog. 2. Generate on every test run. Faster setup, slower CI. Useful when you want fresh data in every test (e.g., to avoid tests accidentally depending on a specific Faker seed). 3. Generate at runtime in a mock server. Tools like [@graphql-tools/mock](https://the-guild.dev/graphql/tools/docs/mocking) integrate directly into Apollo Server or a custom layer, so any query gets typed mocks back without a database.

The first pattern is the most common because it's the simplest. The third is useful for backend-style integration tests where you want a real GraphQL endpoint that just happens to return generated data.

Faker locales: an underused feature

If your product ships in multiple languages, generate mocks in those languages. en-US produces Maya Lindgren. ru produces Russian names. ja produces Japanese ones. The locale also affects addresses, phone numbers, and city names, so a Russian mock dataset has Russian cities — useful for catching layout bugs around long Cyrillic strings or right-to-left languages with Arabic locale.

The GraphQL → TypeScript tool exposes locale as a top-level option. Pick ru and your mocks come back with names like Александр Петров and emails that look believable.

When to write fixtures by hand

Schema-driven mocks aren't always right. A few cases where hand-authored fixtures are better:

  • Edge cases. "User with 100 posts where 99 are deleted." Generators don't know your business rules; you have to write the fixture.
  • Specific test data. "User who has been a paying customer for exactly 18 months." Same problem.
  • Determinism for snapshots. Snapshot tests want byte-identical output across runs. Faker can be seeded for determinism, but committed fixtures are simpler.

Use generated mocks for the 80% case and hand-authored fixtures for the 20% that needs precision.

Where this connects

The Apollo Router 2.14 release covers the production side of GraphQL. graphql-js v17 alpha introduces @defer/@stream which mock generators will eventually need to handle. GraphQL @oneOf is a recent addition to input shapes; mock generators handle it via discriminated unions.

References

Source / further reading: graphql-tools mocking guide