GraphQL utilise un seul endpoint (/graphql) avec des requêtes POST, où la query dans le body de la requête définit exactement les champs à retourner. La structure diffère de REST, mais les tests restent entièrement possibles avec la fixture request de Playwright.
Comment GraphQL diffère de REST
REST : plusieurs endpoints, chacun retournant une structure fixe.
GET /users/1 → { id, name, email, createdAt, ... }
GET /orders/456 → { id, items, total, status, ... }GraphQL : un seul endpoint (/graphql), toujours en POST, vous spécifiez exactement ce que vous voulez :
query {
user(id: "1") {
name
email
}
order(id: "456") {
total
status
}
}La query passe dans le body de la requête sous forme de JSON. La structure de la réponse correspond exactement à votre query : pas de champs superflus, pas de requêtes séparées.
Pour les tests, ça implique que chaque test est un POST vers la même URL et que la structure de la query fait partie du test. Demander un champ inexistant retourne une erreur. De plus, les erreurs GraphQL n'utilisent pas toujours les codes HTTP correspondants (explications ci-dessous).
Écrire des tests GraphQL avec Playwright
Utilisez request.post() pour toutes les opérations GraphQL :
import { test, expect } from '@playwright/test';
test('fetch user returns correct data', async ({ request }) => {
const response = await request.post('https://api.example.com/graphql', {
data: {
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
variables: { id: '1' },
},
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer your-token-here',
},
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.errors).toBeUndefined();
expect(body.data.user.name).toBe('Alice');
expect(body.data.user.email).toContain('@');
});Trois points à retenir :
1. La query utilise des variables ($id: ID!) plutôt que l'interpolation de chaînes. Faites toujours ainsi pour éviter les problèmes d'injection et garder les queries réutilisables.
2. Le header Content-Type doit être application/json.
3. Vérifiez explicitement body.errors, car GraphQL retourne 200 même en cas d'erreur.
Le modèle d'erreur GraphQL
C'est ce qui surprend tous ceux qui testent GraphQL pour la première fois.
REST : une mauvaise requête retourne 400. Une requête non autorisée retourne 401. Une ressource introuvable retourne 404.
GraphQL : presque tout retourne 200. Les erreurs arrivent dans le body de la réponse, en parallèle ou à la place des données.
{
"data": null,
"errors": [
{
"message": "User not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"]
}
]
}Vos assertions doivent donc vérifier le body de la réponse, pas seulement le statut HTTP :
// Faux — passe même quand GraphQL retourne une erreur
expect(response.status()).toBe(200);
// Correct — vérifie le statut HTTP et le champ errors GraphQL
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.errors).toBeUndefined();
expect(body.data).not.toBeNull();Certains serveurs GraphQL retournent bien 400 pour les queries malformées et 401 pour les erreurs d'authentification, mais ne comptez pas dessus. Vérifiez toujours body.errors.
Tester les mutations
Les mutations (opérations de création, mise à jour, suppression) suivent le même schéma :
test('create order mutation', async ({ request }) => {
const response = await request.post('https://api.example.com/graphql', {
data: {
query: `
mutation CreateOrder($input: OrderInput!) {
createOrder(input: $input) {
id
status
total
}
}
`,
variables: {
input: {
productId: 'prod-123',
quantity: 2,
shippingAddress: '123 Main St',
},
},
},
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer your-token-here',
},
});
const body = await response.json();
expect(body.errors).toBeUndefined();
expect(body.data.createOrder.status).toBe('PENDING');
expect(body.data.createOrder.id).toBeTruthy();
});Créer une fixture GraphQL réutilisable
Copier-coller les headers et l'appel POST dans chaque test alourdit rapidement le code. Extrayez-les dans une fixture :
// fixtures/graphql.ts
import { test as base, APIRequestContext } from '@playwright/test';
type GraphQLFixtures = {
gql: (query: string, variables?: Record<string, unknown>) => Promise<Record<string, unknown>>;
};
export const test = base.extend<GraphQLFixtures>({
gql: async ({ request }, use) => {
const gql = async (query: string, variables: Record<string, unknown> = {}) => {
const response = await request.post(process.env.GRAPHQL_URL!, {
data: { query, variables },
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.API_TOKEN}`,
},
});
const body = await response.json();
if (body.errors) {
throw new Error(`GraphQL error: ${JSON.stringify(body.errors)}`);
}
return body.data;
};
await use(gql);
},
});Les tests deviennent alors lisibles :
import { test } from '../fixtures/graphql';
import { expect } from '@playwright/test';
test('fetch user', async ({ gql }) => {
const data = await gql(`
query { user(id: "1") { name email } }
`);
expect(data.user.name).toBe('Alice');
});Tester les cas d'erreur
Vérifiez que votre API GraphQL retourne des erreurs appropriées pour les entrées invalides :
test('returns error for non-existent user', async ({ request }) => {
const response = await request.post('https://api.example.com/graphql', {
data: {
query: `query { user(id: "non-existent-id") { name } }`,
},
headers: { 'Content-Type': 'application/json' },
});
const body = await response.json();
expect(body.errors).toBeDefined();
expect(body.errors[0].message).toContain('not found');
});
test('returns auth error without token', async ({ request }) => {
const response = await request.post('https://api.example.com/graphql', {
data: { query: `query { user(id: "1") { name } }` },
headers: { 'Content-Type': 'application/json' },
// Pas de header Authorization
});
const body = await response.json();
expect(body.errors).toBeDefined();
expect(body.errors[0].message).toMatch(/unauthorized|unauthenticated/i);
});Validation de schéma
Pour aller plus loin dans les tests GraphQL, validez les réponses par rapport au schéma en utilisant graphql-tag et l'introspection de schéma. Ça détecte automatiquement les incompatibilités de types et les changements de champs, utile dans les scénarios de contract testing où le schéma fait office de contrat.
C'est un sujet avancé. La plupart des équipes commencent avec le schéma requête/réponse présenté ci-dessus et ajoutent la validation de schéma plus tard, une fois l'API stabilisée.
→ See also: Tests d'API avec l'APIRequestContext de Playwright (Sans Postman) | Tests d'API Avancés avec Playwright: Patterns pour des Projets Réels | Tests de Contrat avec Pact: Arrêtez de Casser les APIs Entre Équipes