L2 · Engineering literacy
L2Go deeper5 min read

GraphQL and gRPC for testers

Two shapes that break REST-shaped test tooling. GraphQL has one endpoint and no status codes worth asserting on; gRPC is binary and needs generated stubs. Learn how each fails, because the failure modes are what you will be testing.

A REST tester who moves to a team running GraphQL or gRPC often assumes the same testing habits transfer directly, and mostly they do not. Both protocols still run over HTTP underneath, covered in HTTP and REST, but they change what a status code means, where errors live, and what "the response" even is. This guide covers the specific adjustments a tester needs to make for each.

GraphQL: one endpoint, many shapes

GraphQL exposes a single endpoint, usually /graphql, and the client specifies exactly what fields it wants in the request body. This inverts the usual REST testing habit of asserting against a fixed response shape per endpoint. With GraphQL, the shape is defined by the query itself, so the test has to know both the query sent and the exact fields expected back.

The bigger change is in status codes. A GraphQL server frequently returns 200 OK even when the operation failed, with the actual error living in an errors array in the response body alongside a data field that may be partially populated or null. A test suite that only checks response.status === 200 will happily pass tests where the query silently failed.

Query complexity and depth as an attack surface

Because a GraphQL client can request arbitrarily nested data in one query, badly designed schemas allow a single request to trigger enormous database load. This is worth testing directly: send a deeply nested query and confirm the server rejects it with a complexity or depth limit error rather than hanging or timing out. Teams that skip this find out about it during their first real traffic spike, not before.

Schema introspection is another area worth deliberate testing. Most GraphQL servers expose their full schema via an introspection query by default, which is useful in development but a real information leak in production if left enabled. A test asserting introspection is disabled in the production configuration takes minutes to write and prevents a class of bug that is easy to miss in code review.

gRPC: binary, typed, and streaming

gRPC takes the opposite approach from GraphQL's flexibility. It is strictly typed via Protocol Buffers (protobuf), and every request and response shape is defined ahead of time in a .proto file. This makes schema drift easier to catch mechanically. If a field type changes, the generated client code fails to compile, catching the mismatch before a test even runs.

The harder part of testing gRPC is its four call types: unary (one request, one response, like REST), server streaming, client streaming, and bidirectional streaming. Streaming calls need different test patterns entirely. A server-streaming test has to assert on the full sequence of messages received, not just the final one, and has to handle a stream that closes early or drops a message partway through.

inventory.proto
service InventoryService {
  // Unary: standard request/response, closest to REST.
  rpc GetItem(ItemRequest) returns (Item);

  // Server streaming: one request, many responses over time.
  rpc WatchStock(ItemRequest) returns (stream StockUpdate);
}

A worked example: the streaming test that only checked the last message

A logistics team built a gRPC service streaming live stock updates to a warehouse dashboard using the server-streaming pattern above. Consider what happened when their test suite opened the stream, waited for it to close, and asserted the final StockUpdate message matched the expected end state: that check passed reliably in CI for months.

In production, a bug in the stream's buffering logic occasionally dropped messages in the middle of a sequence while still delivering a correct final message, because the final write happened to overwrite the dropped one at the same key. The dashboard flickered incorrect intermediate values for a few seconds before self-correcting, confusing warehouse staff who acted on what they saw. The existing test suite had no way to catch this, since it only ever looked at the last message.

The fix was a test that collected every message in the stream and asserted on the full ordered sequence, not just the tail. That single change caught two more buffering bugs within the first week of being added, both invisible to the old "check the final state" approach.

What actually needs different test design

  • GraphQL: assert on the errors array, not just the HTTP status code, on every request.
  • GraphQL: test query depth and complexity limits explicitly, and confirm introspection is off in production.
  • gRPC: use the generated client from the .proto file rather than hand-rolling request objects, so schema drift fails at compile time.
  • gRPC streaming: assert on the full ordered sequence of messages, not only the final state.
  • Both: authentication still applies the same way it does over REST. The auth: sessions, JWT, OAuth2, OIDC fundamentals carry over unchanged, usually via a header or metadata field rather than a cookie.

FAQ

Questions people ask

Can I reuse my existing REST test framework for GraphQL or gRPC?

Mostly yes for GraphQL, since it is still HTTP with a JSON body, though you need custom assertions for the errors array. gRPC needs protobuf-aware tooling, since the wire format is binary, not JSON.

How do I test GraphQL query complexity limits without guessing at the threshold?

Read the server's configured complexity budget from its schema or documentation, then send a query just under and just over that budget and confirm the boundary behaves as configured.

Is gRPC harder to debug than REST during test failures?

The binary wire format means you cannot just read a raw request in a network tab. Use a tool like grpcurl or BloomRPC to inspect calls in a human-readable form during debugging.

Should introspection ever be enabled in a non-production environment?

Yes, it is genuinely useful in development and staging for exploring the schema. The rule is specifically about disabling it in production, where it becomes a reconnaissance tool for an attacker.