Resource library

QA How-To

Modern GraphQL API Testing Complete Guide (2026)

Use this graphql modern api testing complete guide to test schemas, queries, mutations, security, performance, evolution, subscriptions, and CI safely.

26 min read | 3,100 words

TL;DR

Modern GraphQL API testing combines schema validation, resolver checks, HTTP operations, negative cases, authorization, side effects, complexity controls, subscriptions, and compatibility gates.

Key Takeaways

  • Test schema, resolver, HTTP envelope, and side effects as separate layers.
  • Assert data and errors independently because transport success can include execution failure.
  • Treat nullability, error paths, codes, enums, and coercion as client contracts.
  • Use an identity, tenant, object, and field matrix for authorization.
  • Combine schema diffing with consumer operation replay.
  • Control complexity, batching, and resolver call counts.
  • Use isolated fixtures, ephemeral ports, teardown, and locked dependencies.

A graphql modern api testing complete guide must prove more than whether an endpoint returns HTTP 200. Modern GraphQL quality depends on schema contracts, operation validation, resolver behavior, authorization, nullability, errors, side effects, performance, batching, subscriptions, and safe evolution.

This guide gives you a runnable Node.js lab and a practical 2026 strategy. You will build an Apollo Server, test it through HTTP with Node's test runner, inspect its schema, exercise negative paths, and create a CI gate.

TL;DR

Layer Prove Evidence
Schema The contract is expected Schema assertions and diff
Query Reads return correct data Operations with variables
Mutation Writes validate and persist Response and side-effect checks
Security Access follows policy Identity and tenant matrix
Error Failures remain safe Data, errors, paths, and codes
Performance Cost stays bounded Complexity and call counts
Evolution Consumers remain compatible Diff plus operation replay
Realtime Events reach intended clients WebSocket lifecycle tests

Schema validation is necessary, not sufficient. Type-correct data may still represent the wrong record, leak another tenant, produce excessive calls, or contain unstable errors.

What You Will Build

You will build a Books API and prove:

  • Queries work with variables, aliases, enums, lists, and nullability.
  • Validation, domain, and authorization failures remain distinct.
  • Mutations validate input and preserve state.
  • The schema exposes only intended fields.
  • Fixtures, ports, and teardown stay deterministic.
  • One command runs the suite in CI.

The JavaScript design also applies to TypeScript, GraphQL Yoga, Mercurius, gateways, federation, and managed GraphQL services.

Prerequisites

Install Node.js 22 or newer and npm 10 or newer:

mkdir graphql-api-test-lab
cd graphql-api-test-lab
npm init -y
npm install @apollo/server@5 graphql@16
mkdir src test

Use this package.json:

{
  "name": "graphql-api-test-lab",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node src/server.js",
    "test": "node --test"
  },
  "dependencies": {
    "@apollo/server": "^5.0.0",
    "graphql": "^16.0.0"
  }
}

Run node --version, npm --version, and npm test. Commit package-lock.json so CI resolves the reviewed graph.

Step 1: Define the Contract

Create src/schema.js:

export const typeDefs = `#graphql
  enum BookStatus { DRAFT PUBLISHED }

  type Book {
    id: ID!
    title: String!
    author: String!
    status: BookStatus!
    internalNotes: String
  }

  input AddBookInput {
    title: String!
    author: String!
  }

  type Query {
    book(id: ID!): Book
    books(status: BookStatus): [Book!]!
  }

  type Mutation {
    addBook(input: AddBookInput!): Book!
  }
`;

Nullability is client behavior. A non-null list with non-null members differs from a nullable book lookup. Changing either choice affects generated types and error propagation. Review custom scalars, defaults, directives, descriptions, and deprecation reasons as contract data. Never expose secrets or implementation fields.

Verification note: run node -e "import('./src/schema.js').then(m => console.log(m.typeDefs.includes('addBook')))". Expect true.

Step 2: Implement Deterministic Resolvers

Create src/app.js:

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { GraphQLError } from 'graphql';
import { typeDefs } from './schema.js';

export async function startApp({ port = 0, seed } = {}) {
  const books = structuredClone(seed ?? [
    { id: 'b1', title: 'Reliable APIs', author: 'Mina Cole',
      status: 'PUBLISHED', internalNotes: 'Renew contract' },
    { id: 'b2', title: 'Schema First', author: 'Ravi Shah',
      status: 'DRAFT', internalNotes: 'Editorial review' }
  ]);

  const resolvers = {
    Query: {
      book: (_, { id }) => books.find((book) => book.id === id) ?? null,
      books: (_, { status }) =>
        status ? books.filter((book) => book.status === status) : books
    },
    Book: {
      internalNotes: (book, _, context) => {
        if (context.user?.role !== 'ADMIN') {
          throw new GraphQLError('Not authorized', {
            extensions: { code: 'FORBIDDEN' }
          });
        }
        return book.internalNotes;
      }
    },
    Mutation: {
      addBook: (_, { input }, context) => {
        if (!context.user) {
          throw new GraphQLError('Sign in required', {
            extensions: { code: 'UNAUTHENTICATED' }
          });
        }
        if (input.title.trim().length < 3) {
          throw new GraphQLError('Title must contain at least 3 characters', {
            extensions: { code: 'BAD_USER_INPUT', field: 'title' }
          });
        }
        const book = {
          id: `b${books.length + 1}`,
          title: input.title.trim(),
          author: input.author.trim(),
          status: 'DRAFT',
          internalNotes: null
        };
        books.push(book);
        return book;
      }
    }
  };

  const server = new ApolloServer({ typeDefs, resolvers });
  const { url } = await startStandaloneServer(server, {
    listen: { port },
    context: async ({ req }) => {
      const id = req.headers['x-user-id'];
      const role = req.headers['x-user-role'];
      return { user: id ? { id, role: role ?? 'USER' } : null };
    }
  });
  return { url, stop: () => server.stop() };
}

Port zero prevents parallel collisions. Production authentication must validate credentials rather than trust headers.

Create src/server.js:

import { startApp } from './app.js';
const { url } = await startApp({ port: 4000 });
console.log(`GraphQL API ready at ${url}`);

Verification note: run npm start. Expect the URL on port 4000, then stop with Ctrl+C.

Step 3: Create the HTTP Helper

Create test/helpers.js:

export async function gql(url, {
  query, variables, operationName, headers = {}
}) {
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      accept: 'application/graphql-response+json, application/json',
      ...headers
    },
    body: JSON.stringify({ query, variables, operationName })
  });
  return {
    status: response.status,
    contentType: response.headers.get('content-type'),
    body: await response.json()
  };
}

Pass variables separately and supply operationName for multi-operation documents. Return transport and GraphQL envelopes because they communicate different outcomes. Follow documented GraphQL-over-HTTP status behavior instead of assuming every error has one status.

Verification note: import the module and print typeof gql. Expect function.

Step 4: Test Queries and Nullability

Create test/books.test.js:

import test, { after, before } from 'node:test';
import assert from 'node:assert/strict';
import { startApp } from '../src/app.js';
import { gql } from './helpers.js';

let app;
before(async () => { app = await startApp(); });
after(async () => { await app.stop(); });

test('reads with variables and alias', async () => {
  const result = await gql(app.url, {
    query: `query FindBook($id: ID!) {
      selected: book(id: $id) { id title status }
    }`,
    variables: { id: 'b1' },
    operationName: 'FindBook'
  });
  assert.equal(result.status, 200);
  assert.match(result.contentType, /json/);
  assert.equal(result.body.errors, undefined);
  assert.deepEqual(result.body.data.selected, {
    id: 'b1', title: 'Reliable APIs', status: 'PUBLISHED'
  });
});

test('unknown nullable book is null', async () => {
  const result = await gql(app.url, {
    query: `query { book(id: "missing") { id } }`
  });
  assert.deepEqual(result.body, { data: { book: null } });
});

test('filters by enum', async () => {
  const result = await gql(app.url, {
    query: `query ($status: BookStatus) {
      books(status: $status) { id status }
    }`,
    variables: { status: 'PUBLISHED' }
  });
  assert.deepEqual(result.body.data.books, [
    { id: 'b1', status: 'PUBLISHED' }
  ]);
});

Add empty lists, omitted optional variables, explicit null, Unicode, boundary sizes, every enum, aliases, fragments, and directives based on risk. Prefer focused assertions to giant response snapshots.

Verification note: run npm test. Expect three passes with no open handles.

Step 5: Test Errors and Partial Data

Add:

test('rejects unknown field', async () => {
  const result = await gql(app.url, {
    query: `query { book(id: "b1") { id missingField } }`
  });
  assert.ok(result.body.errors);
  assert.match(result.body.errors[0].message, /Cannot query field/);
  assert.equal(result.body.data, undefined);
});

test('reports coercion failure', async () => {
  const result = await gql(app.url, {
    query: `query ($id: ID!) { book(id: $id) { id } }`,
    variables: { id: null }
  });
  assert.ok(result.body.errors);
  assert.equal(result.body.data, undefined);
});

test('returns partial data for protected field', async () => {
  const result = await gql(app.url, {
    query: `query {
      book(id: "b1") { id title internalNotes }
    }`
  });
  assert.equal(result.body.data.book.id, 'b1');
  assert.equal(result.body.data.book.internalNotes, null);
  assert.equal(result.body.errors[0].extensions.code, 'FORBIDDEN');
  assert.deepEqual(result.body.errors[0].path, ['book', 'internalNotes']);
});

Assert data presence, stable codes, paths, and locations when relevant. Avoid exact human-message matching unless supported. Never leak stacks, tokens, SQL, hosts, or sensitive records. Test null bubbling because one non-null field failure may null a larger parent.

Verification note: filter npm test to these names. Expect three passes and partial usable data beside one FORBIDDEN error.

Step 6: Test Mutations and Effects

Add:

test('creates an authenticated draft', async () => {
  const result = await gql(app.url, {
    query: `mutation Add($input: AddBookInput!) {
      addBook(input: $input) { id title author status }
    }`,
    variables: {
      input: { title: '  Test Design  ', author: 'Ana Wu' }
    },
    headers: { 'x-user-id': 'u1' }
  });
  assert.deepEqual(result.body.data.addBook, {
    id: 'b3', title: 'Test Design', author: 'Ana Wu', status: 'DRAFT'
  });

  const read = await gql(app.url, {
    query: `query { book(id: "b3") { title status } }`
  });
  assert.deepEqual(read.body.data.book, {
    title: 'Test Design', status: 'DRAFT'
  });
});

test('rejects anonymous mutation', async () => {
  const result = await gql(app.url, {
    query: `mutation {
      addBook(input: { title: "Valid", author: "Ana" }) { id }
    }`
  });
  assert.equal(result.body.data, null);
  assert.equal(result.body.errors[0].extensions.code, 'UNAUTHENTICATED');
});

Add invalid inputs and assert BAD_USER_INPUT with field metadata. Real services also need transaction rollback, events, duplicate submission, concurrency, idempotency, and independent persistence checks. Isolate state per test under parallel execution.

Verification note: run npm test, remove the identity header temporarily, and confirm creation fails. Restore it.

Step 7: Gate Schema Evolution

Create test/schema.test.js:

import test from 'node:test';
import assert from 'node:assert/strict';
import { buildSchema } from 'graphql';
import { typeDefs } from '../src/schema.js';

test('schema exposes expected roots', () => {
  const schema = buildSchema(typeDefs);
  const queries = Object.keys(schema.getQueryType().getFields()).sort();
  const mutations = Object.keys(schema.getMutationType().getFields()).sort();
  assert.deepEqual(queries, ['book', 'books']);
  assert.deepEqual(mutations, ['addBook']);
});

test('Book id remains non-null ID', () => {
  const schema = buildSchema(typeDefs);
  const id = schema.getType('Book').getFields().id.type.toString();
  assert.equal(id, 'ID!');
});

Compare a canonical schema against an approved baseline. Review removals, stricter inputs, nullability, enums, scalar meaning, defaults, and directives. Additions may still break exhaustive clients or increase cost. Replay persisted and representative consumer operations. Follow validate GraphQL deprecation with schema diff.

Verification note: run node --test test/schema.test.js. Change ID! to ID temporarily, prove failure, and restore it.

Step 8: Add Security, Performance, and CI Gates

Build an authorization matrix across anonymous users, roles, ownership, tenants, objects, operations, and fields. Test aliases and fragments. Denied mutations must produce no state or event.

Instrument downstream calls to expose N+1 behavior. Assert bounded growth for nested list queries and request-scoped caching. Test depth, aliases, batches, fragments, variables affecting list size, and complexity limits. Use GraphQL query complexity and security testing for adversarial cases.

When arrays are accepted, test mixed outcomes, ordering, size, identity, rate accounting, and cost with GraphQL batched request testing examples.

Verification note: run npm ci and npm test from a clean checkout. CI must fail for skipped required suites, empty selection, schema drift without review, open handles, or test failure.

GraphQL Modern API Testing Complete Guide Checklist

Inventory real client queries, mutations, subscriptions, persisted documents, owners, identities, variables, outputs, errors, effects, dependencies, cost, and compatibility priority. Convert the large schema into a risk model.

For every ID, cover authorized, forbidden, cross-tenant, unknown, malformed when permitted, and null. For pagination, cover empty, one item, boundary, next page, invalid cursor, maximum size, stable ordering, and concurrent changes. For custom scalars, test parsing and serialization.

Exercise fragments, aliases, directives, and multi-operation documents. Fragments can hide protected fields. Aliases can repeat expensive resolution. Directives change execution. Multiple operations must require operationName.

Separate GraphQL validation from domain validation. Missing required variables fail before resolvers. Type-valid business violations reach domain code. Use repository spies to prove whether work occurred.

Verification note: select one critical operation. Require success, GraphQL validation failure, domain failure, and authorization failure, with resolver and side-effect evidence.

Test Federation and Gateways

Validate subgraph schemas, composition, entity keys, reference resolvers, ownership, directives, and the composed schema. A locally passing subgraph may break composition.

Test existing, missing, forbidden, and cross-tenant entity representations. Preserve identity and documented errors through the router. Trace cross-subgraph operations without logging sensitive variables.

Stop optional and required subgraphs separately. Verify partial data, errors, timeouts, and bounded retries. Test slow dependencies to prevent retry storms. Assert downstream call counts for expensive plans.

Check composition before publication, then replay critical consumer documents against the composed graph. Locally additive does not guarantee globally compatible.

Verification note: execute one operation across two subgraphs and confirm data, identity, trace, call count, and optional-subgraph failure behavior.

Test Subscriptions

Cover WebSocket authentication, acknowledgement, operation start, delivery, errors, completion, cancellation, shutdown, and reconnect. Prove event filtering and tenant isolation with matching and nonmatching events.

Avoid long sleeps. Await events with explicit timeouts, use unique run IDs, and close every client. Test ordering, duplicates, queue limits, slow consumers, and recovery according to product promises.

Verification note: subscribe as two tenants, publish one scoped event, assert only the intended client receives it, close both connections, and confirm no open handles. Use the GraphQL subscriptions WebSocket tutorial for the complete lab.

Build Trustworthy Test Evidence

Record what ran, not just what passed. Publish schema revision, operation count, tested roles, environment, application and gateway revisions, and skips. Quarantine needs an owner and deadline.

Pull requests run schema, compatibility, resolver, and hermetic HTTP checks. Pre-release runs gateway, identity, federation, subscription, and complexity checks. Scheduled suites cover load, recovery, and long connections.

Redact credentials and sensitive variables. Retain normalized operation name, safe variables, response envelope, correlation ID, schema revision, and downstream summary. Evidence must help reproduction without creating a security leak.

Track compatibility warnings, protected-field coverage, resolver call growth, and latency by profile. Thresholds depend on consumer importance, abuse risk, and operational budget.

Verification note: inspect a clean CI run. Confirm all required suites ran, revisions are visible, secrets are absent, failures are actionable, and required failure returns nonzero.

Compare the Testing Layers

Level Speed Main value Blind spot
Schema Very high Contract shape Resolver meaning
Resolver unit High Branch behavior HTTP execution
HTTP integration Medium Operation lifecycle Production gateway
Consumer replay Medium Compatibility Unknown consumers
Deployed security Lower Identity boundaries Deep branches
Complexity and load Lower Availability Fine correctness
Subscription Lower Realtime lifecycle Ordinary requests

Use high-signal cases at every layer. Tag by operation, role, risk, and environment. Collect safe telemetry such as normalized operation, duration, code, complexity, and downstream count.

Design a Complete Operation Test Matrix

Build the matrix from the client journey, not from the number of schema fields. One checkout query that joins identity, catalog, promotion, and inventory may deserve more scrutiny than twenty isolated metadata fields. Record the operation owner, business criticality, normal variables, maximum variables, expected error codes, selected sensitive fields, downstream systems, cache policy, and cost limit.

For query arguments, use partitions that reflect GraphQL coercion and domain behavior. Test omitted versus explicit null for optional arguments because resolvers and default values may treat them differently. Test zero, negative, boundary, and excessive numbers where custom validation applies. Test whitespace, Unicode, normalization, case, and maximum length for strings. Test known and unknown enum values at the transport boundary, then test every supported value behaviorally.

Lists require their own matrix. Distinguish a null list, empty list, null member, and omitted field according to schema nullability. Verify stable sorting and pagination cursors with controlled data. Create records that tie on the primary sort field so the secondary order is visible. Request the next page after an insert or delete and assert the documented consistency behavior.

Fragments and directives should not change authorization. Select a sensitive field directly, through a named fragment, through an inline fragment, with an alias, and conditionally through include. The server should apply the same policy every time. For interfaces and unions, test every concrete type plus a newly unknown type scenario in consumer compatibility checks.

Persisted operations add an approval layer. Test known hashes, unknown hashes, a hash paired with mismatched text, registration policy, cache invalidation, and access to retired operations. Confirm clients cannot bypass complexity or authorization by using an approved identifier. Record the persisted registry revision in CI evidence.

Verification note: create a table for one critical query with at least twelve meaningful cases. Every row should state input partition, identity, expected data, expected errors, resolver execution, side effects, and cleanup. Review the table with the API owner before automating it.

Test Authentication, Authorization, and Tenant Isolation

Authentication establishes identity, while authorization decides what that identity may do. Test missing, malformed, expired, revoked, wrong-audience, and insufficient-scope credentials at the deployed gateway. The local header fixture tests application branching, but it cannot prove token validation, cookie policy, CORS, or gateway configuration.

Authorization can occur at operation, object, and field levels. A user may be allowed to query a book but forbidden to see internalNotes. Another user may have the correct role but belong to the wrong tenant. Cover combinations instead of assuming one administrator and one anonymous test represent the policy.

Object identifiers are a common risk. Query an authorized ID, another user's ID, another tenant's ID, a deleted ID, and a random ID. Decide whether forbidden and missing objects intentionally share a response to reduce enumeration. Assert that decision consistently without weakening internal audit evidence.

Mutations need precondition and postcondition checks. For every denied write, verify the database, cache, event stream, audit log policy, and external calls remain unchanged. If the system intentionally audits denied attempts, distinguish that security audit from a business side effect. Test nested input fields that could enable mass assignment.

Caching must preserve identity boundaries. A request-scoped DataLoader is usually safe only when its keys and loader logic enforce tenant and permission rules. Execute the same object query as two identities in sequence and concurrently. Confirm a value loaded for the first user never appears for the second. Repeat the check through any router or response cache.

Introspection policy is not a substitute for authorization. Disabling introspection may reduce convenient discovery in some environments, but attackers can still send valid operations. Sensitive fields need resolver or data-layer policy regardless of introspection settings.

Verification note: run the same protected operation across anonymous, ordinary, owner, wrong-owner, administrator, and cross-tenant identities. Confirm data, errors, paths, codes, side effects, cache results, and safe audit records for each row.

Test Reliability, Caching, and Downstream Failure

Resolvers often depend on databases, REST services, queues, and other subgraphs. Simulate success, not found, timeout, connection failure, malformed upstream data, rate limiting, and partial dependency failure. Map each outcome to a stable GraphQL contract instead of leaking vendor errors.

Timeouts should be bounded by the request budget. If a resolver receives a cancellation signal, verify downstream work stops when the client disconnects or the server deadline expires. Unbounded retries can multiply load during incidents. Test retry count, backoff policy, idempotency, and the final client-visible error with a controllable fake dependency.

Response caches and DataLoader caches solve different problems. Test cache keys for variables, identity, tenant, locale, and relevant headers. Verify mutation invalidation or documented staleness. A cached GraphQL response must never cross an authorization boundary. A DataLoader should batch equivalent keys during one request without becoming a global store.

Partial data needs an intentional user experience. If an optional recommendation service fails, the core book might remain usable with one error. If the authoritative price service fails, returning stale or null price may be unsafe. Encode these choices in nullability, error codes, and tests.

Observe resolver call counts alongside latency. A fast in-memory dependency can hide N+1 behavior until production data or network distance increases. Assert that fetching one hundred books does not cause one hundred author calls when batching is promised. Use representative list sizes around configured limits.

Load tests should use approved environments and owned data. Model typical named operations, a high-cost valid operation, mutation contention, and subscription connections. Report latency and errors by operation profile rather than averaging unrelated work. Never state a universal performance target without the service's budget.

Verification note: inject a timeout into one optional dependency and one required dependency. Confirm bounded duration, cancellation, retry count, data nullability, error path, stable code, logs, metrics, and absence of leaked internal detail.

Maintain the Suite as the Graph Changes

Assign ownership to schema baselines, critical operation documents, fixtures, and specialized suites. A failing compatibility gate needs a consumer-aware decision, not an automatic snapshot update. Record approved breaking changes with migration owners and retirement dates.

Keep test data builders aligned with the schema but do not generate every assertion from the same schema under test. Independent expectations catch defects that self-derived assertions repeat. Use builders for valid defaults, then override the exact fields relevant to each case.

Flaky tests are product signals until proven otherwise. Capture operation name, run ID, application revision, graph revision, seed, safe variables, error paths, and downstream summary. Reproduce under controlled retries, but do not hide persistent failures behind unlimited retry configuration.

Remove coverage only after a field, operation, version, or consumer is truly retired. Search persisted registries, analytics, SDKs, documentation, gateway configuration, scheduled jobs, and rollback plans. Keep a small post-retirement probe when accidental reintroduction would be risky.

Review the matrix when authorization rules, nullability, pagination, error codes, custom scalars, gateways, caches, or downstream ownership changes. A GraphQL suite is a maintained representation of consumer and operational promises, not a one-time collection of requests.

Verification note: schedule a quarterly review of critical operations and immediately review after contract or policy change. The output should identify added cases, retired cases, uncovered consumers, flaky ownership, and baseline decisions.

The Complete Series

Troubleshooting

HTTP is 200 but the case failed -> inspect errors before trusting data.

Tests hang -> stop servers and close pools, timers, consumers, and WebSockets.

IDE works but automation fails -> compare endpoint, media type, variables, operationName, identity, cookies, and gateway route.

Parallel tests fail -> isolate ports, fixtures, users, records, and cleanup ownership.

Authorization leaks -> create context per request and scope caches by identity and tenant.

Schema snapshots churn -> canonicalize and review semantic diffs. Never auto-accept a baseline merely to pass.

Best Practices and Common Mistakes

Name operations, pass variables separately, assert errors and data independently, cover nullability, verify side effects, and use realistic identity matrices. Protect environments with least privilege, allowlisted hosts, unique run IDs, and owned cleanup.

Do not treat introspection as the whole suite, infer success from HTTP alone, snapshot every response, share privileged tokens everywhere, ignore error paths, or benchmark only trivial queries.

Pair this guide with API error handling and negative testing and API contract testing with Pact.

Where To Go Next

Run the lab, then replace one query, protected field, and mutation with critical product operations. Add subscriptions for realtime graphs, complexity tests for public endpoints, schema diffing for many consumers, and batching tests when arrays are accepted. Give every gate an owner and explicit failure policy.

Interview Questions and Answers

Q: Why can GraphQL return HTTP 200 with errors?

Transport can succeed while execution fails. Assert status, data, errors, paths, and stable codes separately.

Q: How do you test a schema?

Ensure it builds, inspect fields and nullability, compare a baseline, and replay consumer operations.

Q: What is null bubbling?

A non-null field failure propagates null to the nearest nullable parent and records its path.

Q: How do you test authorization?

Use identity, role, tenant, ownership, object, operation, and field combinations. Verify denied writes have no effects.

Q: How do you detect N+1?

Instrument downstream calls and assert bounded growth for nested list queries with request-scoped batching.

Q: How do you test compatibility?

Diff the schema and replay consumer operations. Review removals, required inputs, nullability, enums, defaults, and semantics.

Q: What belongs in a mutation test?

Authentication, authorization, coercion, validation, response, persistence, events, duplicates, concurrency, and rollback.

Conclusion

A graphql modern api testing complete guide rests on one idea: a schema defines possibilities, while layered tests prove safe behavior. Combine schema checks, resolver units, HTTP operations, authorization, side-effect checks, consumer replay, complexity controls, and deployed evidence.

Start with the Books API, replace it with one critical product journey, and require npm test in CI. Then extend coverage through the complete series.

Interview Questions and Answers

Why can GraphQL return HTTP 200 with errors?

Transport can succeed while execution reports a field problem. I assert status, data, errors, paths, and stable codes separately.

How do you test a GraphQL schema?

I ensure it builds, inspect fields and nullability, compare an approved baseline, and replay consumer operations.

What is null bubbling?

A non-null field failure propagates null to the nearest nullable parent. I test the boundary and error path.

How do you test authorization?

I use identity, role, tenant, ownership, object, operation, and field combinations. I verify denied writes have no effects.

How do you find N+1 problems?

I instrument downstream calls and assert bounded growth with request-scoped batching.

How do you test backward compatibility?

I diff the schema and replay consumer operations, reviewing inputs, nullability, enums, defaults, and semantics.

What belongs in a mutation test?

Authentication, authorization, coercion, validation, response, persistence, events, duplication, concurrency, and rollback.

Frequently Asked Questions

What is GraphQL API testing?

It verifies schema and query, mutation, and subscription behavior. Complete coverage includes variables, coercion, nullability, errors, authorization, side effects, performance, and compatibility.

Can GraphQL return errors with HTTP 200?

Yes. Transport can succeed while selected fields fail, producing errors and possibly partial data. Assert transport and GraphQL envelopes separately.

How do I test GraphQL queries?

Send named operations with variables, assert the envelope, then verify domain values, nullability, aliases, fragments, and lists. Include missing, invalid, and forbidden cases.

How should mutations be tested?

Test identity, permission, input, response, persistence, side effects, duplicates, concurrency, and rollback. Verify final state independently.

Is schema snapshot testing enough?

No. It exposes structural drift but cannot prove semantics, tenant isolation, performance, persistence, or stable errors. Pair it with behavior tests.

How do you test GraphQL authorization?

Create a matrix for identities, roles, ownership, tenants, objects, operations, and fields. Verify denied reads expose nothing and denied writes have no effect.

How do you test GraphQL performance?

Use representative nested operations, measure latency, count downstream calls, and test complexity, depth, alias, and batch limits.

Related Guides