GraphQL / Intermediate: policy, sources and clients / Lesson 12
Operation tests and contract fixtures
Choose tests that prove coercion, execution shape and policy boundaries without starting a service.
On this page
Lesson goals
- Distinguish pure policy tests, in-process operation tests and HTTP integration.
- Build adversarial fixtures for nullability, input presence and batch ordering.
Test the boundary that can actually fail
A resolver unit test can verify argument forwarding but cannot prove that the composed schema accepts an operation or that null propagation is correct. An in-process GraphQL execution test can cover parsing, validation, coercion and result shape with synthetic services, while still not testing HTTP middleware. Transport integration covers headers, authentication and method handling. Keep these claims distinct. Most domain decisions should have small deterministic tests; a smaller set of operation tests then verifies the adapter contract without depending on a live database or remote endpoint.
Describe a realistic in-process test
The fragment below shows the shape of a GraphQL.js operation test with an already constructed schema and synthetic context. It is deliberately not a complete runnable program because the package and schema fixture are not included. Assert exact selected data, not only the absence of errors; a resolver can return the wrong authorized record without throwing. For denied cases, assert the documented null or error policy and that a write spy was not called. Keep private service internals out of public error snapshots.
const result = await graphql({
schema,
source: "query Card($id: ID!) { course(id: $id) { id title } }",
variableValues: {id: "c1"},
contextValue: syntheticContext
});
assert.deepEqual(result.data, {course:{id:"c1",title:"Graph foundations"}});
assert.equal(result.errors, undefined);
Requires graphql, assert, schema and syntheticContext fixtures. It documents an expected contract and was not executed as an engine test.
Make the failure matrix specific
For a course lookup, test a visible course, absent course, denied course, wrong variable type, missing required variable and a source failure. For a mutation, add omitted/null/value patch inputs, stale revision and timeout-after-commit reconciliation. For a batch adapter, vary result order and missing keys. These tests differ because they target different contracts, not because every field needs the same copied checklist. Choose boundary cases from the fieldโs actual semantics and verify meaningful effects such as no unauthorized write, not just status codes.
Verify patch presence independently of GraphQL
This local function models the summary policy taught earlier. The use of Object.hasOwn distinguishes an omitted member from a present null or empty string. Each assertion checks a different semantic case and the input record is copied rather than mutated in place. It is a pure JavaScript test with no GraphQL package, storage or network. Such a test is narrow but useful: once domain semantics are correct, operation-level tests can focus on whether the GraphQL adapter preserves those semantics during input coercion and service calls.
function applySummary(record, input) {
return Object.hasOwn(input, "summary")
? {...record, summary: input.summary}
: {...record};
}
const old = {id:"c1", summary:"Old"};
console.assert(applySummary(old, {}).summary === "Old");
console.assert(applySummary(old, {summary:null}).summary === null);
console.assert(applySummary(old, {summary:""}).summary === "");
console.assert(old.summary === "Old");
All four assertions pass. The model verifies member presence and nonmutation, not GraphQL input coercion or persistence.
Record what was and was not exercised
Use stable synthetic identifiers, fixed clocks where time matters, and deterministic service fakes. Avoid snapshotting enormous introspection results when a focused compatibility assertion would explain the failure better. Record the implementation version for engine tests because error wording and optional features can differ. This course executes only isolated local JavaScript models and parses JSON fixtures; framework, SDL and transport fragments remain non-runnable. It does not certify a GraphQL engine, HTTP server, database transaction or subscription transport. Honest test scope lets a learner transfer the idea without overestimating the evidence.
Common errors and fixes
A passing resolver test is called an API test
- Why it happens
- Schema composition and coercion were not exercised.
- How to fix it
- Add a focused in-process operation test.
- Prevent it next time
- Label each test by the boundary it verifies.
A snapshot passes while the wrong tenant is returned
- Why it happens
- Assertions checked shape but not identity or policy.
- How to fix it
- Assert the exact eligible record and denied cases.
- Prevent it next time
- Use paired principals against the same fixture.
Flaky timing obscures subscription behavior
- Why it happens
- Tests depended on arbitrary sleep and live delivery.
- How to fix it
- Use deterministic event sources and explicit completion signals.
- Prevent it next time
- Separate local event logic from transport integration.
Check your understanding
1. What does a pure patch test prove that a schema snapshot does not?
Show worked solution
It proves how the chosen domain function distinguishes omitted, null and empty-string values and whether it mutates the original record. A schema snapshot can show declared input structure but not these business semantics. Both can be useful, but an operation test is still needed to verify that the actual GraphQL adapter delivers the right coerced input to the function.
2. Why should a denied mutation test assert no write call?
Show worked solution
An error result alone does not prove that the forbidden side effect never happened; an implementation can write first and reject later. Use a controlled synthetic service spy to assert that authorization failure prevents the write boundary entirely. Also verify the public error policy so the caller learns the intended safe outcome without private source details.
Key takeaways
- Test schema execution, domain decisions and transport as separate boundaries.
- A useful test asserts meaningful data or effects, not only no error.