Playwright est déjà installé. Vous écrivez des tests end-to-end, votre package.json a la dépendance et votre équipe lance npx playwright test en CI. Alors pourquoi ouvrez-vous Postman à chaque fois que vous avez besoin de tester une API ? Playwright embarque un client HTTP complet appelé APIRequestContext, et une fois que vous savez l'utiliser, vous arrêterez de switcher vers un autre outil.
Ce qu'est APIRequestContext et quand l'utiliser
APIRequestContext est le client HTTP intégré de Playwright. Il permet d'envoyer des requêtes GET, POST, PUT, PATCH et DELETE, d'inspecter les réponses, de gérer les en-têtes et cookies. Les assertions sur les codes de statut et les corps de réponse se font directement depuis un fichier .spec.ts.
Ce n'est pas un remplacement des tests UI. Cela sert à un objectif différent. Un test UI pilote le navigateur : il clique sur des boutons, remplit des formulaires, attend des éléments. Un test APIRequestContext envoie des requêtes HTTP directement au serveur, sans navigateur. C'est plus rapide, plus fiable, et mieux adapté pour tester la couche backend.
Quand choisir APIRequestContext plutôt qu'un test UI ?
- Vous testez la validation des données. L'API rejette-t-elle un champ obligatoire manquant ?
- Vous testez l'authentification. Un 401 revient-il si aucun token n'est présent ?
- Vous testez de la logique métier qui vit côté serveur, pas dans l'UI.
- Vous voulez alimenter des données de test avant un test UI sans passer par un formulaire.
- Vous voulez vérifier un effet de bord backend après une action UI.
Quand garder les tests UI ? Quand vous testez ce que l'utilisateur voit et avec quoi il interagit : rendu, navigation, comportement des formulaires, retour visuel. Les deux couches ont leur place dans une suite de tests complète. L'erreur, c'est d'utiliser l'une là où l'autre est clairement meilleure.
La fixture request
Playwright expose APIRequestContext via la fixture intégrée request. Vous l'utilisez exactement comme page : déclarez-la dans la signature de la fonction de test et Playwright gère l'initialisation.
import { test, expect } from '@playwright/test';
test('GET /api/items retourne 200', async ({ request }) => {
const response = await request.get('https://lab.becomeqa.com/api/items');
expect(response.status()).toBe(200);
});Pas de fenêtre navigateur. Pas de rendu DOM. Le test runner envoie une requête HTTP, reçoit une réponse, et vos assertions s'exécutent. Le tout se termine en moins de 100 millisecondes sur une connexion standard.
La fixture request crée un APIRequestContext isolé pour chaque test. Il a son propre cookie jar, ses propres en-têtes, et aucun lien avec un contexte navigateur. Cet isolement est intentionnel : vos tests API sont indépendants de ce que fait le navigateur.
Requêtes GET et assertions sur les réponses
Un test GET comporte trois parties : envoyer la requête, vérifier le statut, vérifier le corps.
import { test, expect } from '@playwright/test';
test('GET /api/items retourne une liste valide', async ({ request }) => {
const response = await request.get('https://lab.becomeqa.com/api/items');
// Vérification du statut
expect(response.status()).toBe(200);
expect(response.ok()).toBeTruthy(); // raccourci : vrai pour tout 2xx
// Parsing du corps JSON
const items = await response.json();
// Vérification de la structure
expect(Array.isArray(items)).toBe(true);
expect(items.length).toBeGreaterThan(0);
// Vérification des propriétés d'un élément
const first = items[0];
expect(first).toHaveProperty('id');
expect(first).toHaveProperty('destination');
expect(first).toHaveProperty('status');
});response.ok() retourne true pour tout code de statut entre 200 et 299. Utilisez-le quand vous avez seulement besoin de confirmer le succès sans vous soucier du code exact. Utilisez response.status() quand le code précis compte : 200, 201 et 204 signifient chacun quelque chose de différent.
Pour lire le corps en texte ou en buffer :
const text = await response.text();
const buffer = await response.body(); // BufferPour asserter sur les en-têtes de réponse :
test('la réponse inclut le content-type JSON', async ({ request }) => {
const response = await request.get('https://lab.becomeqa.com/api/items');
const headers = response.headers();
expect(headers['content-type']).toContain('application/json');
});Requêtes POST avec un corps JSON
Les requêtes POST envoient des données au serveur. Passez le payload via l'option data et Playwright le sérialise automatiquement en JSON avec Content-Type: application/json.
test('POST /api/items crée une ressource', async ({ request }) => {
const response = await request.post('https://lab.becomeqa.com/api/items', {
data: {
destination: 'Kyoto',
status: 'planned',
notes: 'Visiter la forêt de bambous d\'Arashiyama'
}
});
// Une API bien conçue retourne 201 Created pour les nouvelles ressources
expect(response.status()).toBe(201);
const created = await response.json();
expect(created).toHaveProperty('id');
expect(created.destination).toBe('Kyoto');
expect(created.status).toBe('planned');
});Conservez l'id retourné quand vous devez nettoyer ou chaîner des requêtes :
test('créer puis supprimer un élément', async ({ request }) => {
const createRes = await request.post('https://lab.becomeqa.com/api/items', {
data: { destination: 'Tbilissi', status: 'planned' }
});
expect(createRes.status()).toBe(201);
const { id } = await createRes.json();
const deleteRes = await request.delete(`https://lab.becomeqa.com/api/items/${id}`);
expect(deleteRes.status()).toBe(204);
// Confirmer la suppression
const getRes = await request.get(`https://lab.becomeqa.com/api/items/${id}`);
expect(getRes.status()).toBe(404);
});Pour des données form-encodées, remplacez data par form :
const response = await request.post('https://lab.becomeqa.com/api/login', {
form: {
username: 'admin@becomeqa.com',
password: 'testpass123'
}
});Authentification : tokens Bearer et en-têtes personnalisés
La plupart des vraies API sont authentifiées. Les deux patterns les plus courants sont les tokens Bearer et les clés API passées dans les en-têtes.
Token Bearer : connexion d'abord, puis utilisation du token.test('requête authentifiée avec un token Bearer', async ({ request }) => {
// Étape 1 : obtenir un token
const loginRes = await request.post('https://lab.becomeqa.com/api/auth/login', {
data: {
username: 'admin@becomeqa.com',
password: 'testpass123'
}
});
expect(loginRes.status()).toBe(200);
const { token } = await loginRes.json();
// Étape 2 : l'utiliser pour les requêtes suivantes
const itemsRes = await request.get('https://lab.becomeqa.com/api/items', {
headers: {
Authorization: `Bearer ${token}`
}
});
expect(itemsRes.status()).toBe(200);
});Quand le même token est utilisé dans plusieurs tests, déplacez la connexion dans un bloc beforeAll et partagez le token dans la suite :
import { test, expect } from '@playwright/test';
let authToken: string;
test.beforeAll(async ({ request }) => {
const res = await request.post('https://lab.becomeqa.com/api/auth/login', {
data: {
username: 'admin@becomeqa.com',
password: 'testpass123'
}
});
const body = await res.json();
authToken = body.token;
});
test('GET items en tant qu\'utilisateur authentifié', async ({ request }) => {
const response = await request.get('https://lab.becomeqa.com/api/items', {
headers: { Authorization: `Bearer ${authToken}` }
});
expect(response.status()).toBe(200);
});playwright.config.ts.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: 'https://lab.becomeqa.com',
extraHTTPHeaders: {
'X-Api-Key': process.env.API_KEY ?? ''
}
}
});Chaque requête envoyée par la fixture request inclura cet en-tête automatiquement. Pas besoin de le répéter dans chaque test.
.env en local (avec dotenv) et les secrets GitHub Actions en CI. L'accès à process.env de Playwright fonctionne de la même façon dans les deux environnements.playwright.request.newContext() pour les tests API autonomes
La fixture request est pratique dans les tests. Parfois vous avez besoin d'un APIRequestContext en dehors du test runner : dans un fichier de setup global, un script utilitaire, ou un contexte avec sa propre configuration indépendante.
playwright.request.newContext() crée un contexte autonome que vous contrôlez explicitement :
// global-setup.ts
import { chromium, request } from '@playwright/test';
async function globalSetup() {
// Créer un contexte API autonome
const apiContext = await request.newContext({
baseURL: 'https://lab.becomeqa.com',
extraHTTPHeaders: {
'Content-Type': 'application/json'
}
});
// Alimenter les données de test avant le lancement des tests
await apiContext.post('/api/items', {
data: { destination: 'Lisbonne', status: 'planned' }
});
// Toujours disposer après utilisation
await apiContext.dispose();
}
export default globalSetup;Référencez le fichier de setup global dans votre config :
// playwright.config.ts
export default defineConfig({
globalSetup: './global-setup.ts',
use: {
baseURL: 'https://lab.becomeqa.com'
}
});newContext() accepte les mêmes options que le bloc de config use : baseURL, extraHTTPHeaders, httpCredentials, ignoreHTTPSErrors. Appelez dispose() quand le contexte n'est plus nécessaire. Cela ferme les connexions ouvertes et efface les cookies.
playwright.request.newContext() et la fixture request créent tous deux des instances d'APIRequestContext. La différence est le cycle de vie : la fixture est créée et détruite automatiquement par test. newContext() vous donne le contrôle manuel, utile pour le setup global, les scripts de teardown ou les contextes qui s'étendent sur plusieurs tests.Combiner le setup API avec la vérification UI
C'est là que APIRequestContext apporte le plus de valeur. La partie la plus lente et fragile d'un test UI est généralement le setup. Remplir des formulaires, attendre un état, naviguer à travers des écrans juste pour atteindre le scénario à tester. Remplacez ça par un appel API.
test('un élément créé via API apparaît dans la liste UI', async ({ page, request }) => {
// Setup rapide et fiable. Pas de navigateur impliqué.
const createRes = await request.post('https://lab.becomeqa.com/api/items', {
data: { destination: 'Porto', status: 'planned' }
});
expect(createRes.status()).toBe(201);
const { id } = await createRes.json();
// Maintenant tester ce qui compte : l'UI l'affiche-t-elle correctement ?
await page.goto('https://lab.becomeqa.com/items');
await expect(page.getByText('Porto')).toBeVisible();
await expect(page.getByTestId(`item-${id}`)).toBeVisible();
// Nettoyage via API. Plus rapide que de cliquer dans un flux de suppression UI.
await request.delete(`https://lab.becomeqa.com/api/items/${id}`);
});test('supprimer un élément via l\'UI le retire de la base de données', async ({ page, request }) => {
// Créer via API
const createRes = await request.post('https://lab.becomeqa.com/api/items', {
data: { destination: 'La Valette', status: 'planned' }
});
const { id } = await createRes.json();
// Supprimer via l'UI. C'est ce qu'on teste vraiment.
await page.goto('https://lab.becomeqa.com/items');
await page.getByTestId(`item-${id}`).getByRole('button', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
// Vérifier l'état backend, pas seulement l'état UI
const checkRes = await request.get(`https://lab.becomeqa.com/api/items/${id}`);
expect(checkRes.status()).toBe(404);
});Le deuxième pattern est sous-utilisé. Quand un utilisateur supprime quelque chose, l'UI peut se mettre à jour de façon optimiste et paraître correcte même si l'appel API réel a échoué. Asserter au niveau API permet de capturer cet échec.
Helpers API réutilisables et fixtures
Répéter request.post('/api/auth/login', ...) dans chaque fichier de test est du bruit. Construisez une petite classe helper et exposez-la via une fixture personnalisée.
La classe helper :
// lib/api-client.ts
import { APIRequestContext } from '@playwright/test';
export class ApiClient {
constructor(private request: APIRequestContext) {}
async login(username: string, password: string): Promise<string> {
const res = await this.request.post('/api/auth/login', {
data: { username, password }
});
const { token } = await res.json();
return token;
}
async createItem(data: { destination: string; status: string; notes?: string }) {
const res = await this.request.post('/api/items', { data });
expect(res.status()).toBe(201);
return res.json();
}
async deleteItem(id: string) {
await this.request.delete(`/api/items/${id}`);
}
async getItem(id: string) {
return this.request.get(`/api/items/${id}`);
}
}Puis la fixture personnalisée :
// fixtures.ts
import { test as base } from '@playwright/test';
import { ApiClient } from './lib/api-client';
type Fixtures = {
api: ApiClient;
};
export const test = base.extend<Fixtures>({
api: async ({ request }, use) => {
const client = new ApiClient(request);
await use(client);
}
});
export { expect } from '@playwright/test';Les tests deviennent beaucoup plus lisibles :
import { test, expect } from './fixtures';
test('créer et vérifier un élément', async ({ api, page }) => {
const item = await api.createItem({ destination: 'Riga', status: 'planned' });
await page.goto('https://lab.becomeqa.com/items');
await expect(page.getByText('Riga')).toBeVisible();
await api.deleteItem(item.id);
});Le helper encapsule la logique des requêtes. La fixture gère le cycle de vie. Le test se concentre sur le scénario. Chaque couche a un seul rôle.
expect hors de la classe ApiClient, sauf pour les préconditions obligatoires comme vérifier un statut 201 après une création. Les assertions dans les helpers rendent les échecs plus difficiles à tracer, car la stack pointe vers le helper, pas vers le test.request vs page.request : quelle différence
Les deux sont des instances d'APIRequestContext. La distinction porte sur la gestion des cookies et de l'état de session.
request, la fixture, est un contexte isolé. Il a son propre cookie jar, séparé de tout navigateur. Il ne partage pas d'état avec page. Si vous vous connectez via request, le navigateur n'en sait rien.
page.request est lié au contexte navigateur auquel appartient page. Il partage les cookies avec la page. Si l'utilisateur se connecte via le navigateur, page.request transporte ces cookies. Si page.request définit un cookie, le navigateur le voit.
test('différence entre request et page.request', async ({ page, request }) => {
// Connexion via le navigateur
await page.goto('https://lab.becomeqa.com/login');
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Login' }).click();
// page.request transporte le cookie d'auth, retourne 200
const withCookies = await page.request.get('https://lab.becomeqa.com/api/items');
expect(withCookies.status()).toBe(200);
// La fixture request n'a pas de cookie, retourne 401
const withoutCookies = await request.get('https://lab.becomeqa.com/api/items');
expect(withoutCookies.status()).toBe(401);
});Lequel utiliser ? Si votre test implique à la fois une session navigateur et des appels API qui doivent utiliser la même authentification, utilisez page.request. Pour des tests API purs sans navigateur, utilisez la fixture request. Si vous avez besoin d'un contexte totalement indépendant avec des en-têtes ou une base URL personnalisés, utilisez playwright.request.newContext().
FAQ
Ai-je encore besoin de Postman ?Postman est un bon outil d'exploration. Quand vous découvrez une API et ne connaissez pas encore sa structure, ouvrez Postman, explorez, lisez les réponses, comprenez ce dont vous avez besoin. Une fois que vous savez quoi tester, écrivez-le dans Playwright. Vous obtenez la gestion de version, l'intégration CI et la possibilité de combiner des assertions API et UI dans le même test, ce que Postman ne vous donne pas.
Puis-je utiliserAPIRequestContext pour tester GraphQL ?
Oui. GraphQL via HTTP est une requête POST vers un endpoint unique avec un corps JSON contenant query et optionnellement variables. L'option data le gère directement :
const response = await request.post('https://lab.becomeqa.com/graphql', {
data: {
query: `
query GetItem($id: ID!) {
item(id: $id) {
id
destination
status
}
}
`,
variables: { id: '123' }
}
});
const { data } = await response.json();
expect(data.item.destination).toBe('Kyoto');APIRequestContext suit-il automatiquement les redirections ?
Oui, par défaut il suit jusqu'à 20 redirections. Pour désactiver le suivi des redirections et inspecter la réponse de redirection directement, passez maxRedirects: 0 dans les options de la requête.
Définissez un timeout personnalisé dans les options de la requête ou augmentez timeout dans votre playwright.config.ts pour le projet API. Pour les APIs avec limites de débit en test, envisagez d'alimenter les données dans globalSetup une seule fois plutôt que de les créer dans chaque test.
response.json() et response.text() ?
response.json() parse le corps et retourne un objet JavaScript. Il lève une erreur si le corps n'est pas du JSON valide. response.text() retourne la chaîne brute. Utilisez text() pour le débogage ou quand l'endpoint retourne un format non-JSON comme du texte brut ou du XML.
→ See also: Fixtures Playwright Expliquées: Des Intégrées aux Personnalisées | Configuration et Nettoyage Global dans Playwright | Tests d'API avec Playwright: Au-delà de l'Interface | Tests d'API Avancés avec Playwright: Patterns pour des Projets Réels | Authentification dans les Tests d'API: Clés API, Tokens Bearer, OAuth2, JWT