GraphQL: Code Examples (Python + Pydantic + Playwright)
Practical code examples for testing GraphQL APIs. Uses Pydantic for response validation and Playwright for HTTP requests.
1. Pydantic Models for GraphQL Responses
These models validate the shape and types of GraphQL responses. Use them in every test to catch unexpected changes early.
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
class GraphQLError(BaseModel):
"""Single error entry from GraphQL response."""
message: str
locations: Optional[list[dict]] = None
path: Optional[list[str | int]] = None
extensions: Optional[dict] = None
class UserNode(BaseModel):
id: str
name: str
email: str
created_at: datetime
class PostNode(BaseModel):
id: str
title: str
body: str
author: UserNode
class UserQueryResponse(BaseModel):
"""Validates the full GraphQL response envelope."""
data: Optional[dict] = None
errors: Optional[list[GraphQLError]] = None
class UsersListData(BaseModel):
users: list[UserNode]
Key points:
- UserQueryResponse handles both success and error cases in one model.
- Optional fields allow partial responses (GraphQL can return data + errors together).
- Pydantic raises ValidationError if the response shape does not match — test fails fast.
2. Playwright GraphQL Client Fixture
Playwright's APIRequestContext sends HTTP requests without a browser. Good for API testing.
import pytest
from playwright.sync_api import Playwright, APIRequestContext, APIResponse
BASE_URL = "https://api.example.com/graphql"
@pytest.fixture(scope="session")
def gql_context(playwright: Playwright) -> APIRequestContext:
"""Create a reusable GraphQL client for the test session."""
context = playwright.request.new_context(
base_url=BASE_URL,
extra_http_headers={
"Content-Type": "application/json",
"Authorization": "Bearer test-token",
},
)
yield context
context.dispose()
def gql_query(
context: APIRequestContext,
query: str,
variables: dict | None = None,
) -> APIResponse:
"""Send a GraphQL query and return the raw response."""
payload: dict = {"query": query}
if variables:
payload["variables"] = variables
response = context.post("", data=payload)
return response
Key points:
- Session-scoped fixture reuses one HTTP connection for all tests.
- gql_query helper keeps tests clean — each test only defines query + variables.
- Authorization header is set once in the fixture.
3. Query and Mutation Tests
Test: fetch a user by ID
def test_query_user(gql_context: APIRequestContext) -> None:
query = """
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
created_at
}
}
"""
response = gql_query(gql_context, query, {"id": "user-123"})
assert response.status == 200
body = UserQueryResponse.model_validate(response.json())
assert body.errors is None
assert body.data is not None
Test: query depth limit rejects deep nesting
def test_query_depth_limit(gql_context: APIRequestContext) -> None:
deep_query = """
{
user(id: "1") {
posts { comments { author { posts { comments {
author { name }
} } } } }
}
}
"""
response = gql_query(gql_context, deep_query)
assert response.status == 200
body = UserQueryResponse.model_validate(response.json())
assert body.errors is not None
Test: introspection disabled in production
def test_introspection_disabled(gql_context: APIRequestContext) -> None:
query = "{ __schema { types { name } } }"
response = gql_query(gql_context, query)
assert response.status in (200, 400)
body = response.json()
assert "errors" in body
Test: mutation creates a user
def test_mutation_create_user(gql_context: APIRequestContext) -> None:
mutation = """
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
"""
variables = {
"input": {"name": "Alice", "email": "alice@test.com"},
}
response = gql_query(gql_context, mutation, variables)
assert response.status == 200
body = response.json()
assert body.get("errors") is None
assert body["data"]["createUser"]["name"] == "Alice"
Key points:
- Every test validates status code AND response body.
- Pydantic model_validate catches schema drift automatically.
- Depth limit test proves the server rejects abusive queries.
- Introspection test verifies production security configuration.