How do you make a schema-derived GraphQL mock return domain-shaped, repeatable values?
answer
- Defaults are correct but meaningless
- A map keyed by type name
- Your fields plus type-derived rest
- Array length decides list length
- Kill randomness at the source
basics
~20 sSupply override functions keyed by type name - the mock merges what you return with type-derived defaults for the fields you left out - and remove randomness at the source, with a seeded generator or pinned values.
solid answer
~50 sTwo mechanisms, solving two different problems. **Overrides**: you pass a map from type name to a function returning a partial object for that type - `Money` returns `"4820.75"`, `Claim` returns `{ status: 'IN_REVIEW' }` - and the mock fills in every field you did not mention from its declared type. Keying by type rather than by response path matters, because the value then follows the type everywhere it appears, including into fields added next quarter; a per-test override at the root field is layered on top when one scenario needs pinning. **Determinism**: default mocks often randomise ids, numbers and list lengths, which makes any snapshot assertion churn on every run. Fix it at the source - seed the random generator with a constant, or return constants from overrides for the fields the assertion actually reads. Returning an array of a chosen length is also how you force an empty list or a long one.
code
pseudocode · 14 lines// base, shared by every test in the suite
baseMocks = {
Money: () -> "4820.75",
DateTime: () -> "2026-02-11T09:14:00Z", // never now()
ID: () -> nextIdForType(), // counter, reset per test
ClaimStatus: () -> "IN_REVIEW",
}
// one test: a settled claim with no documents at all
scenarioMocks = merge(baseMocks, {
Query: () -> { claim: { status: "SETTLED", documents: [] } }
})
mockServer = mockFromSchema(schemaSdl, scenarioMocks, seed = 4177)go deeper
Know that you replace a mock's default value by supplying your own function for a type, and that the schema itself is never edited to make a test produce the data you want.
Explain the merge semantics precisely: your override supplies some fields, type defaults fill the rest, and per-test overrides layer on top of a shared base. Be able to say why unpinned randomness and whole-payload assertions cannot coexist.
Demonstrate a house style - a small type-keyed base every test inherits, scenario overrides only for the case under test, determinism guaranteed at the source. Treat a flaky mock-backed test as a defect in the fixture, not a retry candidate.
Decide where override maps live and who maintains them. One shared fixture module drifts from the schema over time, while per-test copies duplicate domain rules the schema never enforced - name the tradeoff you are choosing and the review that keeps it honest.
## Why type defaults are not enough A schema-derived mock gets you a correctly-shaped response for free, but the values in it are deliberately meaningless. On an insurance claims graph that shows up fast. A `Money` custom scalar has no derivable value at all. A `status` field cycles through enum members with no relation to what the screen under test is meant to show. A `[ClaimDocument!]!` field always returns the same two items, so the empty state and the overflow state are never rendered. The first thing anyone does with a mock is start controlling it. ## The override map The standard mechanism is a map from **type name** to a function that returns a partial object for that type: ``` mocks = { Money: () => "4820.75", ClaimStatus: () => "IN_REVIEW", Claim: () => ({ reference: "CLM-77419", documents: [{}, {}, {}] }), } ``` The semantics that matter are **merge, not replace**. Your `Claim` function names three fields; every other field of `Claim` that the document selects is still filled from its declared type. That is what keeps overrides small: you state only the parts the test cares about, and the mock stays responsible for completeness. A candidate who thinks the override replaces the whole object writes fixtures that break every time a field is added to the type. Overrides also compose downward. `Claim.settlementAmount` has type `Money`, so the `Money` override supplies its value without `Claim` mentioning it at all. This is precisely why the map is keyed by type: one entry for `Money` gives every monetary field in the graph a well-formed value, forever, including fields nobody has written yet. A map keyed by response path - `claim.settlementAmount` - fires for exactly one selection and silently falls back to defaults the moment a fragment moves the field somewhere else. Path-shaped and root-shaped overrides still have a place. For a single test that needs one specific scenario - a denied claim with no settlement - overriding the root `claim` field to return exactly that object is clearer than bending the type-level defaults. The house pattern that scales is a small type-keyed base every test inherits, plus a per-test override layered on top for the scenario under test. ## Controlling list length Collection screens need three shapes: empty, typical, and too many. Mocking layers commonly take the **length of the array you return** as the number of items to generate, filling each element from the item type. So `documents: () => []` gives the empty state, `documents: () => [{}, {}, {}]` gives three, and an array of twenty exercises the overflow. Keep those local to the tests that need them, or the default response grows into something nobody can read. ## Determinism, and why snapshots expose the lack of it A mock that generates a fresh identifier or a random number on every call is fine while you are eyeballing a screen and fatal once an assertion compares a whole payload. The failure looks like this: a snapshot test that has never been edited fails on every run, with a diff showing an id or a list length changing and nothing else. The test is not asserting your component; it is asserting the mocking layer's random source. There are three honest fixes, and they are not equivalent: 1. **Seed the generator.** If the mocking layer draws from a random source you can seed, fix the seed to a constant and the same document produces the same bytes every run. This is the cleanest option because it leaves the rest of the payload realistic. 2. **Pin the values the assertion reads.** Return constants from overrides for those fields. Narrower than seeding, and it survives a change of mocking layer. 3. **Narrow the assertion.** If a snapshot covers values nobody meant to assert, the snapshot is the wrong tool; assert the handful of fields that carry meaning instead. A useful middle path when values must be distinguishable but stable is a **counter per type**: `Claim-1`, `Claim-2`, `ClaimDocument-1`, reset between tests. Constant placeholders make a mis-keyed list look correct, because every row is identical; a counter makes the same bug obvious while still being repeatable. Whichever you choose, the reset has to happen in test setup - a counter that carries across test files reintroduces order-dependence in a new costume. One last rule that is worth stating plainly: never reach for the wall clock inside a mock value function. A `DateTime` derived from `now()` is a non-determinism source that survives seeding, and it produces the specially annoying kind of failure that only reproduces after midnight. ## What none of this changes Overrides make a mock's values look like your domain. They do not make the mock *know* your domain: nothing stops an override map from producing a combination of fields that no real record could hold, and nothing in it is checked against the server. That gap is the subject of the next question.
- Why key mock overrides by type name rather than by the path of the field in the document?Because a type appears in many places and a path appears in one. Keying `Money` by type gives every monetary field in every document a well-formed value, including fields added later. A path-keyed override fires only for the exact selection you wrote, so a document reshaped by moving a fragment silently falls back to defaults. Path-shaped overrides are still useful for pinning one scenario in one test, but they belong on top of a type-keyed base rather than in place of it.
- A snapshot test against a mock fails on every run with no code change. Where do you look first?At whatever the mock generates that nobody pinned. The four usual suspects are random identifiers, random numbers, values taken from the wall clock, and randomly-sized lists. Fix it at the source by seeding the generator, by returning constants from overrides for the fields the snapshot actually reads, or by replacing a whole-payload snapshot with assertions on the few fields that carry meaning. Rerunning until it passes is not a fix.
- How would you make one mocked list empty and another long, in the same suite?Return arrays of the right length from that field's override - mocking layers commonly use the length of the array you return to decide how many items to generate, filling each element from the item type. An empty array gives the empty state and an array of twenty gives the overflow state, with no schema change. Keep both overrides local to the tests that need them so the shared default stays small and readable.
saying these in an interview costs you the question
- Edits the schema so a mock returns test data
- Thinks a type override replaces the whole object
- Overrides every field by hand in every test
- Snapshots a mock without pinning random values
- Calls the wall clock inside a mock value function
- Reruns a flaky mock-backed test until it passes