Guides

How to Test GraphQL APIs: Techniques & Tools (2026)

Parveen KumariUpdated Aug 19, 20268 min read

Quick answer

Testing a GraphQL API means sending a POST with a JSON query and variables to a single endpoint (typically /graphql), then checking the response in two places: the data field for correct results, and the errors array for resolver-level failures — GraphQL almost always returns HTTP 200 even when a query fails, so a status-code-only assertion misses real errors entirely. Because it's just a POST with JSON, every framework already covered here (pytest, REST Assured, Playwright, k6, Postman) tests GraphQL with no new library.

Reviewed by Smeet Gohel

Share:
Bar chart showing REST responses spread across many status codes while GraphQL responses are almost always 200

Testing a GraphQL API means sending a POST request with a JSON body — a query string and variables object — to a single endpoint, then checking the response in two places instead of one: the data field for correct results, and the errors array for resolver-level failures. That second part is the detail most REST-testing habits miss, since GraphQL returns HTTP 200 for almost every syntactically valid request, error or not.

This guide covers what's actually different about GraphQL testing, working code in the frameworks already covered in this series, and the tools built specifically for GraphQL.

Table of Contents

  1. What's Different About Testing GraphQL
  2. Testing GraphQL with the Frameworks You Already Have
  3. Testing Mutations
  4. The errors Array: Testing Failure Cases
  5. Query Depth and Complexity Limiting
  6. Dedicated GraphQL Tools
  7. Common Pitfalls in GraphQL Testing
  8. FAQ

Testing a GraphQL API by checking both the data field and the errors array in the response body

What's Different About Testing GraphQL

Three structural differences from REST drive everything else in this guide:

  • One endpoint, not many. Every operation — queries, mutations, subscriptions — goes to the same URL (commonly /graphql), differentiated by the request body, not the path.
  • The status code doesn't tell you what happened. A GraphQL server typically returns 200 for any syntactically valid request, whether the operation succeeded, partially succeeded, or failed at the resolver level. The real result lives in the body.
  • Clients choose exactly which fields to fetch. This solves REST's over-fetching/under-fetching problem by design, but means the same endpoint can return meaningfully different response shapes depending on what the client asked for — your tests need to match the query's actual field selection, not a fixed expected shape.

Testing GraphQL with the Frameworks You Already Have

Because a GraphQL request is just a POST with a JSON body, every framework already covered in this series works with no new library:

# pytest + requests
def test_graphql_get_user(api_session, base_url):
    query = """
    query GetUser($id: ID!) {
      user(id: $id) {
        id
        name
        email
      }
    }
    """
    response = api_session.post(
        f"{base_url}/graphql",
        json={"query": query, "variables": {"id": "1"}},
        timeout=10,
    )
    body = response.json()
    assert response.status_code == 200
    assert "errors" not in body
    assert body["data"]["user"]["id"] == "1"
// k6
const query = `
  query GetUser($id: ID!) {
    user(id: $id) { id name email }
  }
`;

export default function () {
  const res = http.post(
    'https://api.example.com/graphql',
    JSON.stringify({ query, variables: { id: '1' } }),
    { headers: { 'Content-Type': 'application/json' } }
  );
  check(res, {
    'status is 200': (r) => r.status === 200,
    'no errors': (r) => !JSON.parse(r.body).errors,
  });
}
// REST Assured
@Test
void getUserViaGraphQL() {
    String body = """
        {"query": "query GetUser($id: ID!) { user(id: $id) { id name email } }", "variables": {"id": "1"}}
        """;

    given()
        .contentType("application/json")
        .body(body)
    .when()
        .post("/graphql")
    .then()
        .statusCode(200)
        .body("errors", nullValue())
        .body("data.user.id", equalTo("1"));
}

In Postman, select GraphQL as the request body type instead of raw JSON — Postman renders a dedicated query editor and, when introspection is enabled on the target server, autocompletes fields directly from the live schema.

Ready to shift left with your API testing?

Try our no-code API test automation platform free. Generate tests from OpenAPI, run in CI/CD, and scale quality.

Testing Mutations

Mutations follow the identical request shape — a query field in the body actually contains a mutation operation:

def test_create_post_mutation(api_session, base_url):
    mutation = """
    mutation CreatePost($title: String!, $body: String!) {
      createPost(title: $title, body: $body) {
        id
        title
      }
    }
    """
    response = api_session.post(
        f"{base_url}/graphql",
        json={"query": mutation, "variables": {"title": "foo", "body": "bar"}},
        timeout=10,
    )
    body = response.json()
    assert response.status_code == 200
    assert "errors" not in body
    assert body["data"]["createPost"]["title"] == "foo"

The errors Array: Testing Failure Cases

A negative test case in GraphQL asserts on the errors array's presence and content, not a 4xx status code:

def test_get_nonexistent_user_returns_error(api_session, base_url):
    query = """
    query GetUser($id: ID!) {
      user(id: $id) { id name }
    }
    """
    response = api_session.post(
        f"{base_url}/graphql",
        json={"query": query, "variables": {"id": "999999"}},
        timeout=10,
    )
    body = response.json()
    assert response.status_code == 200  # still 200
    assert len(body["errors"]) > 0
    assert body["errors"][0]["path"] == ["user"]

Each error object typically includes a message, a path identifying which field in the query failed, and an extensions object many servers use for a machine-readable error code — check what your specific GraphQL server populates there and assert on it, since message text alone is a brittle thing to assert against.

Query Depth and Complexity Limiting

GraphQL's ability to nest queries arbitrarily deep is also a denial-of-service vector unique to it — a client can construct a single query requesting deeply nested, expensive-to-resolve data in one call. Test that your server actually rejects queries past its configured depth or complexity limit:

def test_excessively_deep_query_is_rejected(api_session, base_url):
    deep_query = "query { user(id: \"1\") { posts { comments { author { posts { comments { author { id } } } } } } } }"
    response = api_session.post(
        f"{base_url}/graphql",
        json={"query": deep_query},
        timeout=10,
    )
    body = response.json()
    assert len(body.get("errors", [])) > 0

This is a genuinely GraphQL-specific check — nothing in a REST API's design creates an equivalent single-request amplification risk this large.

Dedicated GraphQL Tools

  • GraphiQL / Apollo Sandbox — in-browser query explorers, typically bundled with a GraphQL server for manual, ad-hoc exploration with schema-aware autocomplete via introspection.
  • Postman — native GraphQL request body type with schema-aware autocomplete, usable for both manual exploration and, via Newman, CI-integrated automated tests.
  • Insomnia — also has first-class GraphQL support with a dedicated query editor, similar to Postman's.
  • Apollo Studio — a schema registry and monitoring platform for Apollo-based GraphQL APIs, useful for tracking schema changes and query performance over time rather than per-request assertions.

Common Pitfalls in GraphQL Testing

Free Guided worksheet

Build Your Testing Strategy in 30 Minutes

A structured worksheet that walks you through defining your testing strategy in 30 minutes. Cover architecture, tools, layers, and team responsibilities.

Download Free
  • Asserting only the status code. It's almost always 200 — check the errors array explicitly for both positive and negative cases.
  • Writing a query that requests more fields than the test actually asserts on. Match the query's field selection to what the test checks — an over-broad query makes the test slower and the intent less clear.
  • Forgetting to test query depth/complexity limits. This is the GraphQL-specific security check that has no REST equivalent.
  • Leaving introspection enabled in production without deciding to. Confirm whether it's intentionally on (useful for API-consuming partners) or should be restricted — don't leave it as an accidental default.
  • Treating a partial-success response as a full failure or full success. GraphQL can return both data (for the fields that resolved) and errors (for the ones that didn't) in the same response — test that your client and assertions handle that correctly, not just the all-or-nothing case.

Frequently Asked Questions

How is testing a GraphQL API different from testing a REST API? One endpoint instead of many, and HTTP 200 for almost every request — real success or failure lives in the data and errors fields of the response body, not the status code.

Do I need a special tool to test GraphQL APIs? No — since it's a POST with a JSON body, any HTTP-capable framework (pytest, REST Assured, Playwright, k6, Postman) tests GraphQL with no new library.

Why does my GraphQL API return 200 even when there's an error? Standard behavior — resolver-level failures go into the response body's errors array instead of the HTTP status code.

What is query depth limiting and why should I test it? A check that the server rejects excessively nested queries, which can otherwise be used to construct a single, extremely expensive request — a GraphQL-specific denial-of-service vector.

Should introspection be enabled when testing a GraphQL API? Yes in development/test, where it powers autocomplete in tools like GraphiQL and Postman. In production it's commonly disabled or restricted since it exposes your full schema.

Can Total Shift Left generate tests for GraphQL APIs? Yes — REST, SOAP, and GraphQL are all supported for full test generation. gRPC is adapter-only, not included in that list.

Key Takeaways

  • Check data and errors both — a 200 status code alone proves almost nothing in GraphQL.
  • No new tooling required. pytest, REST Assured, Playwright, k6, and Postman all test GraphQL directly.
  • Negative test cases assert on the errors array, not a 4xx status code.
  • Query depth/complexity limiting is a GraphQL-specific security test with no REST equivalent — don't skip it.
  • A response can be partially successfuldata and errors can both be populated in the same response.

Generate GraphQL Test Coverage Automatically

Hand-writing query, mutation, and error-path tests for a growing GraphQL schema scales the same way any hand-written suite does. Total Shift Left generates test coverage for REST, SOAP, and GraphQL APIs directly from your schema, including negative and boundary cases — regenerated automatically as your schema evolves.

Start your free trial to see generated coverage for your own GraphQL API, or see plans and pricing if you're already evaluating.

Ready to shift left with your API testing?

Try our no-code API test automation platform free.