mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 17:22:38 +02:00
8.5 KiB
8.5 KiB
GraphQL Testing
Table of Contents
When to use: Testing GraphQL APIs — queries, mutations, variables, and error handling.
Patterns
Basic Query with Variables
All GraphQL requests go through POST to a single endpoint. Send query, variables, and optionally operationName in the JSON body.
import { test, expect } from "@playwright/test";
const GQL_ENDPOINT = "/graphql";
test("query with variables", async ({ request }) => {
const resp = await request.post(GQL_ENDPOINT, {
data: {
query: `
query FetchItem($id: ID!) {
item(id: $id) {
id
title
price
reviews { id rating }
}
}
`,
variables: { id: "101" },
},
});
expect(resp.ok()).toBeTruthy();
const { data, errors } = await resp.json();
// GraphQL returns 200 even on errors — always check both
expect(errors).toBeUndefined();
expect(data.item).toMatchObject({
id: "101",
title: expect.any(String),
price: expect.any(Number),
});
expect(data.item.reviews).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
rating: expect.any(Number),
}),
])
);
});
Mutations
import { test, expect } from "@playwright/test";
const GQL_ENDPOINT = "/graphql";
test("mutation creates resource", async ({ request }) => {
const resp = await request.post(GQL_ENDPOINT, {
data: {
query: `
mutation AddItem($input: ItemInput!) {
addItem(input: $input) {
id
title
status
}
}
`,
variables: {
input: {
title: "New Widget",
price: 15.0,
status: "DRAFT",
},
},
},
});
const { data, errors } = await resp.json();
expect(errors).toBeUndefined();
expect(data.addItem).toMatchObject({
id: expect.any(String),
title: "New Widget",
status: "DRAFT",
});
});
Validation Errors
import { test, expect } from "@playwright/test";
const GQL_ENDPOINT = "/graphql";
test("handles validation errors", async ({ request }) => {
const resp = await request.post(GQL_ENDPOINT, {
data: {
query: `
mutation AddItem($input: ItemInput!) {
addItem(input: $input) { id }
}
`,
variables: { input: { title: "" } },
},
});
const { data, errors } = await resp.json();
expect(errors).toBeDefined();
expect(errors.length).toBeGreaterThan(0);
expect(errors[0].message).toContain("title");
expect(errors[0].extensions?.code).toBe("BAD_USER_INPUT");
});
Authorization Errors
import { test, expect } from "@playwright/test";
const GQL_ENDPOINT = "/graphql";
test("handles authorization errors", async ({ request }) => {
const resp = await request.post(GQL_ENDPOINT, {
data: {
query: `
query AdminDashboard {
adminMetrics { revenue activeUsers }
}
`,
},
});
const { data, errors } = await resp.json();
expect(errors).toBeDefined();
expect(errors[0].extensions?.code).toBe("UNAUTHORIZED");
expect(data?.adminMetrics).toBeNull();
});
Authenticated GraphQL Fixture
// fixtures/graphql-fixtures.ts
import { test as base, expect, APIRequestContext } from "@playwright/test";
type GraphQLFixtures = {
gqlClient: APIRequestContext;
adminGqlClient: APIRequestContext;
};
export const test = base.extend<GraphQLFixtures>({
gqlClient: async ({ playwright }, use) => {
const ctx = await playwright.request.newContext({
baseURL: "https://api.myapp.io",
extraHTTPHeaders: {
Authorization: `Bearer ${process.env.API_TOKEN}`,
"Content-Type": "application/json",
},
});
await use(ctx);
await ctx.dispose();
},
adminGqlClient: async ({ playwright }, use) => {
const loginCtx = await playwright.request.newContext({
baseURL: "https://api.myapp.io",
});
const loginResp = await loginCtx.post("/graphql", {
data: {
query: `
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) { token }
}
`,
variables: {
email: process.env.ADMIN_EMAIL,
password: process.env.ADMIN_PASSWORD,
},
},
});
const { data } = await loginResp.json();
if (!data?.login?.token) {
throw new Error(`Admin login failed: status ${loginResp.status()}, response: ${JSON.stringify(data)}`);
}
await loginCtx.dispose();
const ctx = await playwright.request.newContext({
baseURL: "https://api.myapp.io",
extraHTTPHeaders: {
Authorization: `Bearer ${data.login.token}`,
"Content-Type": "application/json",
},
});
await use(ctx);
await ctx.dispose();
},
});
export { expect };
GraphQL Helper Function
// utils/graphql.ts
import { APIRequestContext, expect } from "@playwright/test";
export async function gqlQuery<T = any>(
request: APIRequestContext,
query: string,
variables?: Record<string, any>
): Promise<{ data: T; errors?: any[] }> {
const resp = await request.post("/graphql", {
data: { query, variables },
});
expect(resp.ok()).toBeTruthy();
return resp.json();
}
export async function gqlMutation<T = any>(
request: APIRequestContext,
mutation: string,
variables?: Record<string, any>
): Promise<{ data: T; errors?: any[] }> {
return gqlQuery<T>(request, mutation, variables);
}
// tests/api/items.spec.ts
import { test, expect } from "@playwright/test";
import { gqlQuery, gqlMutation } from "../../utils/graphql";
test("fetch and update item", async ({ request }) => {
const { data: fetchData } = await gqlQuery(
request,
`query GetItem($id: ID!) { item(id: $id) { id title } }`,
{ id: "101" }
);
expect(fetchData.item.title).toBeDefined();
const { data: updateData, errors } = await gqlMutation(
request,
`mutation UpdateItem($id: ID!, $title: String!) {
updateItem(id: $id, title: $title) { id title }
}`,
{ id: "101", title: "Updated Title" }
);
expect(errors).toBeUndefined();
expect(updateData.updateItem.title).toBe("Updated Title");
});
Anti-Patterns
| Don't Do This | Problem | Do This Instead |
|---|---|---|
Check only response.ok() |
GraphQL returns 200 even on errors — errors array is the real signal |
Always check both data and errors in the response body |
Ignore errors array |
Validation and auth errors appear in errors, not HTTP status |
Destructure and assert: expect(errors).toBeUndefined() |
| Hardcode query strings inline everywhere | Duplicated queries are hard to maintain | Extract queries to constants or use a helper function |
| Skip variable validation | Invalid variables cause cryptic server errors | Validate input shape before sending |
Troubleshooting
GraphQL returns 200 but data is null
Cause: GraphQL servers return HTTP 200 even when the query has errors. The actual error is in the errors array.
Fix: Always destructure and check both data and errors.
const { data, errors } = await resp.json();
if (errors) {
console.error("GraphQL errors:", JSON.stringify(errors, null, 2));
}
expect(errors).toBeUndefined();
expect(data.item).toBeDefined();
"Cannot query field X on type Y"
Cause: The field doesn't exist in the schema, or you're querying the wrong type.
Fix: Verify the schema. Use introspection or check your GraphQL IDE for available fields.
// Introspection query to debug schema
const { data } = await request.post("/graphql", {
data: {
query: `{ __type(name: "Item") { fields { name type { name } } } }`,
},
});
console.log(data.__type.fields);
Variables not being applied
Cause: Variable names in the query don't match the variables object keys, or types don't match.
Fix: Ensure variable names match exactly (case-sensitive) and types align with the schema.
// Wrong: variable name mismatch
const resp = await request.post("/graphql", {
data: {
query: `query GetItem($itemId: ID!) { item(id: $itemId) { id } }`,
variables: { id: "101" }, // Should be { itemId: "101" }
},
});
// Correct
const resp = await request.post("/graphql", {
data: {
query: `query GetItem($itemId: ID!) { item(id: $itemId) { id } }`,
variables: { itemId: "101" },
},
});