Resource library

QA How-To

Testing a GraphQL mutation (2026)

Learn testing a GraphQL mutation through response, state, side-effect, security, retry, and concurrency checks, with runnable Node.js tests for QA engineers.

22 min read | 4,145 words

TL;DR

Testing a GraphQL mutation requires assertions across the GraphQL envelope, returned payload, durable state, side effects, and security boundary. Add input, retry, concurrency, and failure tests according to business risk, because HTTP 200 alone proves very little.

Key Takeaways

  • A successful HTTP exchange does not prove a successful GraphQL mutation, always inspect both data and errors.
  • Define the business invariant first, then verify the response, durable state, and downstream effects independently.
  • Use GraphQL variables and named operations so coercion, diagnostics, and automation remain reliable.
  • Test authentication, object ownership, field permissions, and tenant isolation separately.
  • Retry and concurrency tests must prove one intended resource and one intended business effect.
  • Fault injection is most useful when it targets known boundaries before commit, after commit, and during event delivery.
  • Layer schema checks, component tests, deployed API checks, and a small end-to-end suite in CI.

Testing a GraphQL mutation means proving more than a successful HTTP exchange. A reliable test confirms the operation accepted the intended input, returned the promised GraphQL shape, changed durable state exactly once, enforced authorization, and produced only the expected downstream effects.

That wider view matters because a mutation can return HTTP 200 while its response contains GraphQL errors, or it can return plausible data even though persistence, an event, or an audit record failed. This guide gives QA and SDET engineers a repeatable method, plus runnable Node.js tests that use standard fetch and node:test APIs.

TL;DR

Evidence layer What to verify Typical failure caught
Transport Method, headers, media type, status policy Proxy or content negotiation defect
GraphQL envelope data, errors, operation path Resolver or validation failure hidden by HTTP 200
Mutation payload ID, status, normalized fields, domain error Incorrect resolver mapping
Durable state Independent read after the mutation Response says success but no commit occurred
Side effects Events, emails, jobs, audit entries Duplicate or missing integration work
Security Role, ownership, field permissions Broken object-level authorization
Retry behavior Same intent under retries and overlap Duplicate records or effects

The minimum useful mutation test asserts the response and then reads the changed resource through an independent path. High-risk operations also need controlled evidence from the event, payment, inventory, email, or audit boundary.

1. Define Testing a GraphQL Mutation Through Invariants

Start with a business invariant, not a status code. For a mutation named createCandidateProfile, a useful invariant might be: one authorized request creates one profile owned by the caller, returns its stable identifier, and emits one CandidateProfileCreated event. For changeApplicationStatus, the invariant might require an allowed transition, one audit entry, and no modification when the caller lacks permission.

Write the invariant before selecting a tool. It tells you which evidence is authoritative. A response assertion can prove what the resolver reported, but it cannot prove a database commit. A follow-up query can prove observable state, but it may not prove that a message was published only once. A test-only event sink or queryable outbox can provide that evidence without exposing production internals.

Separate three concepts that teams often mix together:

  • GraphQL success means the response follows the expected data and errors contract.
  • Business success means the requested state transition completed according to domain rules.
  • Delivery success means required downstream effects were accepted or durably scheduled.

Decide whether the mutation promises atomic behavior. If a profile update and notification are one transaction from the user's perspective, partial completion is a defect. If the API promises eventual notification after a committed update, the response may succeed while the notification remains pending. The test must then poll a controlled status with a deadline instead of demanding immediate delivery.

This is the central discipline of testing a GraphQL mutation: state the invariant, identify the authoritative observation points, and assert each promise at the correct time.

2. Understand the Mutation Request and Response Contract

A GraphQL mutation is an operation document sent with optional variables and an optional operationName. Over HTTP, POST with a JSON body is the interoperable choice for state-changing operations. The JSON object uses the established keys query, variables, and operationName. Variables should be a JSON object, not an array or a string containing JSON.

The evolving GraphQL-over-HTTP draft identifies application/json for request bodies and prefers application/graphql-response+json for responses while retaining application/json compatibility. Because that document remains a draft, test the service's published HTTP contract and do not treat every draft recommendation as universal server behavior.

GraphQL execution results require body-aware assertions. A well-formed response can contain:

  • data with the mutation payload and no errors.
  • data plus errors when field execution partially fails.
  • errors without usable data when parsing, validation, operation selection, or variable coercion prevents execution.

Do not write assert status === 200 and stop. With legacy JSON response handling, many GraphQL failures still use 200. Conversely, gateways can return non-GraphQL errors before the request reaches the service. Inspect Content-Type, parse only supported formats, and record a sanitized body when parsing fails.

Name every automated operation. mutation CreateCandidate($input: CreateCandidateInput!) gives logs and traces a stable operation name. Pass values through variables rather than string interpolation. This lets GraphQL perform type coercion, avoids broken quoting, keeps documents reusable, and reduces accidental injection in test code.

Mutation fields execute serially at the top level within one operation, but that does not create a universal database transaction. Resolver implementations and downstream services determine atomicity. Test the documented business boundary rather than assuming GraphQL supplies it.

3. Build a Risk-Based Mutation Test Matrix

Derive partitions from the schema and the domain state machine. If an input contains a non-null email, optional display name, enum role, and list of skills, cover valid representatives plus omission, explicit null, invalid enum, empty list, maximum list size, duplicate items, wrong scalar type, and domain-invalid combinations. GraphQL validation and business validation are different layers, and both deserve explicit expectations.

Use a matrix that connects scenario, response, state, and effects:

Scenario GraphQL expectation State expectation Side-effect expectation
Valid owner request Payload returned, no unexpected errors One intended change Required effects once
Missing required input Request error or typed input error No change None
Domain conflict Stable domain code Existing state unchanged None
Unauthorized role Safe denial No change Security audit only if specified
Duplicate retry Same result or documented conflict No duplicate resource No duplicate effect
Dependency timeout Typed failure or accepted async state Atomic or documented partial state Reconciled once
Concurrent stale update One documented winner Version invariant holds Effects match winner

Prioritize irreversible and high-value mutations such as payments, deletion, permission changes, account recovery, inventory reservation, and hiring decisions. A cosmetic preference mutation still needs coverage, but it does not require the same failure-injection depth as changing an administrator role.

Trace each case to a risk or acceptance criterion. Avoid generating one test for every schema field combination without judgment. Pairwise or property-based generation can help with broad inputs, but named examples should protect critical business rules and produce readable failures.

For test data design, see API test data management strategies. Stable ownership, cleanup, and unique identifiers prevent mutation tests from colliding in CI.

4. Create a Runnable Node.js GraphQL Test Harness

The following helper uses APIs built into current Node.js: global fetch, AbortSignal.timeout, the stable node:test runner, and strict assertions. Save it as graphql-mutation.test.mjs. Set a test endpoint and token, then run node --test graphql-mutation.test.mjs.

import test from 'node:test';
import assert from 'node:assert/strict';

const endpoint = process.env.GRAPHQL_URL;
const token = process.env.GRAPHQL_TEST_TOKEN;

assert.ok(endpoint, 'GRAPHQL_URL is required');
assert.ok(token, 'GRAPHQL_TEST_TOKEN is required');

async function executeGraphQL({ query, variables, operationName }) {
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      authorization: `Bearer ${token}`,
      'content-type': 'application/json',
      accept: 'application/graphql-response+json, application/json;q=0.9',
    },
    body: JSON.stringify({ query, variables, operationName }),
    signal: AbortSignal.timeout(10_000),
  });

  const contentType = response.headers.get('content-type') ?? '';
  assert.match(contentType, /application\/(graphql-response\+json|json)/i);

  const payload = await response.json();
  return { response, payload };
}

const createCandidate = `
  mutation CreateCandidate($input: CreateCandidateInput!) {
    createCandidate(input: $input) {
      candidate {
        id
        email
        displayName
        status
      }
      userErrors {
        code
        field
        message
      }
    }
  }
`;

const candidateById = `
  query CandidateById($id: ID!) {
    candidate(id: $id) {
      id
      email
      displayName
      status
    }
  }
`;

test('authorized user creates one candidate with observable state', async () => {
  const unique = `${Date.now()}-${crypto.randomUUID()}`;
  const input = {
    email: `qa+${unique}@example.test`,
    displayName: 'Mutation Test Candidate',
  };

  const created = await executeGraphQL({
    query: createCandidate,
    variables: { input },
    operationName: 'CreateCandidate',
  });

  assert.equal(created.response.status, 200);
  assert.equal(created.payload.errors, undefined);
  assert.deepEqual(created.payload.data.createCandidate.userErrors, []);

  const candidate = created.payload.data.createCandidate.candidate;
  assert.ok(candidate.id);
  assert.equal(candidate.email, input.email.toLowerCase());
  assert.equal(candidate.status, 'DRAFT');

  const readBack = await executeGraphQL({
    query: candidateById,
    variables: { id: candidate.id },
    operationName: 'CandidateById',
  });

  assert.equal(readBack.payload.errors, undefined);
  assert.deepEqual(readBack.payload.data.candidate, candidate);
});
GRAPHQL_URL=https://qa.example.test/graphql \
GRAPHQL_TEST_TOKEN=replace-with-short-lived-test-token \
node --test graphql-mutation.test.mjs

The schema names are illustrative, so replace them with operations from your test service. The Node methods are real and require no third-party package. Add an approved host allowlist before using shared credentials, never print the token, and restrict the test account to a disposable QA tenant.

5. Verify Happy Paths Across Response, State, and Effects

A happy-path test should be narrow enough to diagnose but complete enough to prove the contract. Assert the exact mutation field, required payload fields, stable identifiers, normalization rules, default state, and an empty typed error collection if the schema uses one. Avoid snapshotting the entire response when trace fields, timestamps, or newly added optional fields can change safely.

Read the resource back through a query, a public API, or another supported interface. An independent read is better than trusting a response created from in-memory objects before commit. Where read-after-write consistency is eventual, poll a specific condition with a deadline and a short interval. Report the final observed state on timeout. Do not use a long fixed sleep.

Then verify side effects at controlled boundaries. Examples include:

  • One outbox record with the correct aggregate ID and event type.
  • One fake email accepted for the intended recipient and template.
  • One inventory reservation for the requested quantity.
  • One audit entry containing actor, action, target, and correlation ID.
  • No payment provider call for a mutation that only saves a draft.

Prefer observable test interfaces over direct production database access. A component test may inspect a repository and message publisher directly. An end-to-end test should use externally meaningful evidence or a secure test support API. Too much database coupling makes tests pass even when user-visible behavior is broken.

Check normalization deliberately. If the service lowercases email addresses, trims display names, or canonicalizes phone numbers, assert the documented representation. If it preserves user input, do not force a normalization preference into the test. Also confirm server-generated fields such as ownership and creation time cannot be overridden through unexpected input members.

Finally, clean up using a supported mutation or a unique tenant partition. Cleanup must not hide the original failure. Register it after creation succeeds, attempt it in teardown, and retain the primary assertion error if both test and cleanup fail.

6. Test Variables, Input Coercion, and Domain Validation

GraphQL input coercion happens before resolver execution for many invalid values. A required variable omitted entirely differs from an optional variable omitted, and explicit null differs from omission when defaults apply. Test these distinctions because clients, generated SDKs, and manual requests can produce each form.

For a mutation input object, include cases for unknown fields, missing non-null fields, null nested objects, incorrect scalar types, invalid enum values, oversized numbers, empty strings, Unicode, boundary lengths, duplicate collection members, and mutually exclusive fields. Custom scalars such as DateTime, EmailAddress, or Decimal need their documented lexical and semantic boundaries. The scalar name alone does not define a universal format.

Classify expected failures:

  • Parse failure: the operation document is not valid GraphQL syntax.
  • Validation failure: the document references an unknown field, wrong argument, or incompatible variable type.
  • Coercion failure: runtime variable values cannot satisfy declared input types.
  • Domain validation: input is structurally valid but violates a business rule.
  • Conflict: current state makes the requested transition invalid.

For the first three classes, prove the mutation resolver and its side effects were not invoked. A component test can spy on the repository or publisher. An API test can query by a unique business key and inspect a controlled event sink. For domain errors, follow the schema contract. Some services use top-level GraphQL errors, while others return typed userErrors within data. Do not insist on one style, but require stable machine-readable codes and safe messages.

Test multiple invalid fields to understand whether the service returns all detected input errors or stops at the first. Assert only the documented ordering. Error text often changes, so prefer code and field path for automation while still reviewing messages for clarity and sensitive-data leakage.

Use GraphQL API testing fundamentals for broader query, schema, and transport coverage that complements mutation-specific state checks.

7. Test Authentication and Authorization at Every Object Boundary

Authentication proves the caller identity. Authorization decides whether that caller may execute the mutation, target a particular object, change particular fields, and request sensitive output fields. A valid token is not evidence that object-level authorization works.

Build a role and ownership matrix with anonymous, invalid-token, expired-token, normal owner, normal non-owner, manager, and administrator cases as relevant. Change only the target ID or tenant ID between owner and non-owner tests. Assert a safe denial, no state change, and no business side effect. If security auditing is required, verify a denial record without exposing secrets in the test report.

GraphQL makes field-level checks important in both input and output. A user might be allowed to update displayName but not role, tenantId, approvalStatus, or salaryBand. Try forbidden fields through direct input, aliases, fragments, nested inputs, and old field names retained for compatibility. The server must not silently accept and ignore a privileged change unless that behavior is explicitly documented and safe.

Object identifiers are not authorization. Test sequential-looking IDs and opaque IDs alike. A caller must not mutate another user's object simply by learning its ID from a list, URL, log, or prior account. Also test batch-style mutations so one authorized item does not cause unauthorized neighbors to be processed.

Inspect the response selection. A caller authorized to perform an action may still be unauthorized to read every returned field. Request restricted fields alongside public ones and assert the schema's partial-data policy without leaking values through error messages.

For a wider security checklist, use API security testing with OWASP. Run destructive authorization tests only in approved environments with disposable accounts and records.

8. Test Idempotency, Retries, and Concurrent Mutations

Clients retry when responses are lost, gateways time out, mobile connections change, or workers restart. A mutation that creates a charge, order, application, or reservation needs an explicit retry contract. GraphQL itself does not make mutations idempotent.

If the input accepts an idempotency key or client mutation ID, generate it once per logical action and reuse it for retries. Send the same operation twice over separate connections. Assert one stable resource identity and one business effect. Then reuse the key with different meaningful input and expect a documented conflict, not silent replay or a second execution.

For updates, use a version, revision, or expected timestamp when the domain supports optimistic concurrency. Read version 4, send two updates that both expect version 4, and assert the documented winner or conflict behavior. Final state and events must match the accepted update. Last-write-wins may be valid for a low-risk preference, but it should be an intentional contract.

Concurrency tests need more than two sequential calls. Start requests behind a barrier or use a component-level hook that pauses both after reading state. Exercise duplicate creation, competing transitions, delete versus update, and permission revocation during an operation. Repeat against multiple service instances so an in-memory lock does not create false confidence.

Do not turn concurrency correctness into uncontrolled load. A small deterministic interleaving provides better evidence than thousands of requests with unknown overlap. Query final state by a unique key, count downstream effects, and preserve correlation IDs for both calls.

The detailed API idempotency testing guide explains key scope, payload binding, retention, and commit-boundary failures that also apply to GraphQL mutations.

9. Exercise Partial Failures and Transaction Boundaries

Mutation failure tests should target boundaries before validation, before commit, after commit but before response, and during downstream delivery. Each location creates a different retry and reconciliation risk. Work with developers to expose deterministic fault injection in a component or isolated integration environment.

Suppose submitApplication writes the application, publishes an event, and returns the object. If the publisher fails before a database transaction commits, the expected result may be no application and a retryable error. If an outbox record commits atomically with the application, the response can succeed or fail according to contract while delivery proceeds later. If the application commits but the process dies before responding, a client retry must find the original outcome rather than create a duplicate.

GraphQL null propagation adds response nuance. If a nullable optional field fails during completion, the response can contain useful mutation data plus an error path. If a non-null field fails, null can propagate to the nearest nullable parent. Assert the data subtree, null location, error path, machine code, and durable state together. Never interpret a null payload as proof that no state changed.

Test dependency timeouts, explicit rejection, malformed dependency data, duplicate callbacks, delayed callbacks, and circuit-breaker rejection. Use bounded delays and controlled fakes. Random network disruption produces flakes without guaranteeing the intended boundary was reached.

After every forced failure, query the aggregate, idempotency record if observable, outbox, and side-effect sink. Classify the outcome as rolled back, committed and pending, committed and delivered, or inconsistent. An inconsistent result needs a product decision and often a reconciliation mechanism, not merely another test retry.

Redact input values and tokens from diagnostics. Preserve operation name, sanitized variables, response errors, correlation ID, revision, and observed state so a failure is reproducible.

10. Validate Events, Audit Trails, and Deletion Semantics

Many mutations are valuable because of what happens after the response. Define event assertions by business meaning, not by raw serialized bytes. Verify event type, aggregate ID, actor, tenant, schema version, correlation or causation ID, and the fields consumers require. Ignore transport-generated metadata unless it is part of the contract.

Test exactly-once business effect even when delivery is at least once. A message broker can redeliver, so downstream consumers need idempotent handling. In a controlled test, deliver the same event twice and confirm one notification, ledger entry, or projection change. That is different from asserting the publisher emitted once.

Audit records require special care. A successful privilege change should record who initiated it, what target changed, the before and after security-relevant values, time, and request correlation. A denied attempt may require a security event but must not record a successful business action. Audit evidence should be immutable to the tested user and should not contain credentials or full sensitive payloads.

Deletion mutations need explicit semantics. Determine whether deletion is soft, hard, scheduled, or anonymizing. Assert visibility through normal queries, administrator views, search indexes, caches, and related records according to policy. A soft-deleted object must not remain mutable through an older mutation. A hard deletion may still preserve a minimal compliance audit record.

Also test cancellation and restoration if supported. Confirm their allowed state transitions and event order. Race deletion against update and read operations to reveal stale caches or resurrection. Cleanup code should not use the same deletion path as the behavior under test when that would erase diagnostic evidence before assertions finish.

11. Cover Schema Evolution, Performance, and Observability

Schema compatibility checks catch breaking signatures before runtime. Diff the proposed schema against an accepted baseline and validate real consumer operation documents. Pay attention to removed mutation fields, newly required arguments, changed input nullability, removed enum values, output nullability changes, and custom scalar behavior. Deprecation gives clients time to migrate, but it does not make removal safe by itself.

Behavior can break without a schema change. A resolver may reinterpret an enum, change default ownership, emit a different event, reorder validation, or stop trimming input. Keep focused behavioral tests for critical semantics even when contract validation passes.

Performance testing should use controlled data and a stated service objective. Measure the mutation response separately from asynchronous completion. Track dependency call counts, database statements where observable, queue delay, and error rate. One mutation with a nested response selection can trigger expensive field resolution after the state change, so compare minimal and realistic selection sets.

Observability is part of testability. A mutation trace should connect operation name, authenticated subject, tenant, resolver work, database transaction, outbox or publisher, and response without recording raw tokens or sensitive variables. Logs should distinguish validation rejection, authorization denial, conflict, dependency failure, and internal defect. Metrics should avoid unbounded labels such as candidate ID or email.

Test tracing and logging behavior with a synthetic correlation ID if the service accepts one. Verify it is returned or discoverable according to contract and appears at required boundaries. Confirm a maliciously long or newline-containing value cannot forge log entries.

Run performance and fault cases in a dedicated environment. Pull requests should get deterministic contract and critical behavior checks, while scheduled jobs can cover deeper concurrency, dependency degradation, and migration compatibility.

12. Operationalize Testing a GraphQL Mutation in CI

Organize the suite by feedback speed and evidence. Unit tests exercise input mapping, domain transitions, authorization policies, and resolver-to-service contracts. Component tests use a real database plus controlled dependencies to prove transactions, idempotency, and outbox behavior. API tests verify the deployed schema, HTTP exchange, identity boundary, and observable state. A small end-to-end layer confirms the highest-value user journeys.

On every pull request, run schema compatibility, operation validation, resolver or component tests, and a critical mutation smoke suite. On the main branch, add the role matrix, retry cases, old-client operations, and broader data boundaries. Scheduled or pre-release jobs can add deterministic concurrency, fault injection, migration rehearsal, and performance checks.

Give every flaky test an owner, failure category, and expiry for quarantine. Do not automatically retry assertion failures until green. A retry can be useful diagnostic evidence for environment instability, but the first failure must remain visible. Separate product failure, environment failure, test defect, and data collision in reporting.

Keep secrets in the CI secret store, issue short-lived credentials where possible, and mask headers and variables. Bind test users to isolated tenants. Fail closed if the target host is not allowlisted. Use unique data, tag created records with a run ID, and provide a safe cleanup job for abandoned runs.

Publish concise evidence: operation name, build revision, sanitized variables, response envelope, correlation ID, read-back state, and controlled side-effect counts. This makes testing a GraphQL mutation useful as a release signal rather than a collection of opaque green checks.

Interview Questions and Answers

Q: Why is HTTP 200 insufficient when testing a GraphQL mutation?

GraphQL can return a well-formed response with errors even when the HTTP exchange succeeds. I inspect data and errors, assert the mutation payload, and then verify durable state. For important operations I also inspect downstream effects, because the response alone cannot prove a commit or event delivery.

Q: How do you test whether a mutation actually persisted data?

I read the resource through an independent query or supported API after the mutation. I compare stable business fields and identity, accounting for documented eventual consistency with a bounded poll. In a component test I can additionally inspect the repository transaction, but external behavior remains the primary end-to-end oracle.

Q: How do GraphQL validation errors differ from domain errors?

GraphQL validation and variable coercion can reject a request before the resolver executes. Domain errors occur after structurally valid input reaches business logic, such as an invalid status transition. I assert the correct error location and prove rejected cases made no state change or side effect.

Q: How would you test authorization for a mutation?

I build a role and ownership matrix, then vary target identity independently of token identity. I test operation, object, field, and tenant boundaries, including aliases, fragments, and old input fields. Every denial must leave state and business effects unchanged, with safe error details.

Q: What should be tested when a mutation is retried?

I first identify the documented idempotency mechanism. I repeat the same logical request over separate connections, verify one resource and one effect, then reuse the key with different input and expect a conflict. I also test a lost response after commit and concurrent duplicates.

Q: How do you test partial data in a mutation response?

I assert the surviving data, the null location, the error path and code, and the actual durable state. Null propagation depends on schema nullability, so I identify the nearest nullable parent. I never assume a null mutation payload means rollback occurred.

Q: Where do mutation tests belong in the test pyramid?

Domain transitions and authorization rules belong mostly in unit tests. Transactions, idempotency, and publisher boundaries fit component tests, while deployed API tests cover HTTP, schema, identity, and observable state. A few end-to-end tests cover critical integrations without making the whole suite slow.

Q: How would you diagnose a flaky mutation test?

I preserve the operation name, sanitized variables, correlation ID, response, state read-back, and side-effect evidence. Then I classify timing, shared data, eventual consistency, dependency, environment, and assertion causes. I replace fixed sleeps with bounded condition polling and remove collisions before considering retries.

Common Mistakes

  • Asserting only the HTTP status and ignoring GraphQL errors.
  • Checking returned data without independently verifying persistence.
  • Assuming all top-level mutation fields share one database transaction.
  • Treating a valid token as proof of object and field authorization.
  • Interpolating test values directly into the operation document.
  • Snapshotting volatile responses instead of asserting business invariants.
  • Using long fixed sleeps for eventual state or event delivery.
  • Retrying with a new idempotency key and calling it an idempotency test.
  • Forcing random dependency failures instead of targeting a known commit boundary.
  • Printing tokens, personal data, or complete variables in CI logs.
  • Running destructive security and concurrency tests against uncontrolled data.
  • Removing deprecated mutation fields without validating real client operations.

Conclusion

Testing a GraphQL mutation is a layered proof. Validate the request and response envelope, assert the mutation payload, read durable state independently, and observe the effects that matter to the business. Add negative input, authorization, retry, concurrency, and failure-boundary coverage according to risk.

Start with one critical mutation and write its invariant in plain language. Build the smallest test that proves response, state, and effects, then expand the matrix around the failures that could create incorrect, duplicated, or unauthorized outcomes.

Interview Questions and Answers

Why is HTTP 200 not enough for a GraphQL mutation test?

GraphQL can report validation or execution problems in the errors array while the HTTP request succeeds. I inspect data and errors together, assert the mutation payload, then verify durable state. For critical operations I also verify downstream effects.

What is your test oracle for a GraphQL mutation?

I start with a business invariant and identify authoritative evidence for the response, persisted state, and side effects. The oracle might combine a mutation payload, a follow-up query, and a controlled event sink. I avoid treating one response object as proof of every layer.

How do you test invalid mutation input?

I separate parse, validation, variable coercion, domain validation, and state conflict cases. I derive boundaries from input types and business rules. For every rejected case, I prove the resolver or business effect did not run.

How would you test mutation authorization?

I use roles, tenants, ownership, and field permissions as independent dimensions. I test direct IDs, nested inputs, aliases, fragments, and old compatibility fields. A denial must expose no protected data and create no protected side effect.

How do you test mutation retries?

I clarify the idempotency contract, reuse one client key for the same logical action, and send retries over separate connections. I verify stable resource identity, one durable effect, payload binding, and safe behavior after a lost response. I also test concurrent duplicates.

What does partial data mean after a mutation?

A field can fail during execution while other nullable parts of the response remain available. I assert the surviving fields, null propagation, error path, and machine code. I independently check whether the business state committed.

Where should GraphQL mutation tests run in CI?

Unit tests cover domain rules, component tests cover transactions and publishers, and deployed API tests cover HTTP, schema, identity, and observable state. Pull requests get critical deterministic cases. Scheduled jobs can add concurrency, fault injection, and performance checks.

How do you diagnose a flaky GraphQL mutation test?

I preserve the operation name, sanitized variables, response, correlation ID, read-back state, and side-effect evidence. Then I classify data collision, eventual consistency, dependency, environment, or assertion causes. I replace fixed sleeps with bounded condition polling.

Frequently Asked Questions

How do you test a GraphQL mutation?

Send a named mutation with variables, inspect both data and errors, and assert the typed payload. Then read the changed resource independently and verify required side effects such as events, emails, audit entries, or inventory changes.

Can a failed GraphQL mutation return HTTP 200?

Yes. Many GraphQL services, especially those using legacy application/json response behavior, can return HTTP 200 with an errors array. Tests must follow the service contract and always inspect the response body.

Should GraphQL mutation tests verify the database?

They should verify durable state, preferably through an independent supported API or query. Component tests can inspect repositories directly, but end-to-end tests should emphasize externally meaningful evidence and controlled side-effect interfaces.

How do you test GraphQL mutation authorization?

Use a matrix of roles, tenants, and object ownership. Change the target independently from the token identity, request restricted input and output fields, and prove every denial causes no protected state change or business effect.

Are GraphQL mutations idempotent?

Not automatically. The API needs an explicit idempotency or concurrency contract for retry-sensitive actions. Tests should repeat one logical action with the same key and verify one resource and one effect.

How do you test partial errors in a mutation?

Assert surviving data, the null location, error path and code, and the actual durable state. GraphQL null propagation depends on schema nullability, so a null payload does not by itself prove rollback.

Which tool is best for GraphQL mutation automation?

Any standards-compliant HTTP client can work. The article uses Node.js fetch and node:test because they are current built-in APIs, but the important parts are readable operation documents, variables, layered assertions, safe credentials, and deterministic data.

Related Guides