Une API qui rend une 200 sur le bon chemin mais avec un mauvais corps casse autant que si elle rendait une 500. Le problème, c’est que les tests bout-en-bout via le navigateur ne le voient pas toujours : le frontend ignore parfois les champs manquants, ou les remplit avec une valeur par défaut. Tester l’API directement, sans navigateur, attrape ces bugs avant qu’ils n atteignent le client. Playwright 1.59.1 inclut une fixture request dédiée qui transforme le runner en client HTTP de qualite production. Ce tutoriel construit pas-à-pas une suite de tests d’API : authentification, GET, POST, codes d’erreur, et intégration dans la CI.
Prerequis
- Node.js 20 ou plus, idéalement Node 24 (Krypton).
- Une API REST a tester qui démarre en local sur un port connu, ou un environnement de test accessible.
- Playwright 1.59 installé dans le projet (
npm init playwright@latestsi ce n’est pas déjà fait). - Niveau attendu : connaissances HTTP de base (verbes, statuts, headers, JSON).
- Temps total : 1h30 pour parcourir et adapter au projet.
Etape 1 — Comprendre la fixture request
Playwright exposé deux objets importants pour les tests : page (un onglet de navigateur, utilisé pour le E2E) et request (un client HTTP pur, sans navigateur). Pour un test d’API, on n’a pas besoin de page du tout. Cela rend le test 5 a 10 fois plus rapide qu un equivalent E2E, et évite tous les problèmes lies au rendu (CSS, animations, attente d hydratation).
import { test, expect } from '@playwright/test';
test('GET /api/health repond 200', async ({ request }) => {
const response = await request.get('/api/health');
expect(response.status()).toBe(200);
expect(await response.json()).toMatchObject({ status: 'ok' });
});
Le test ne charge aucun navigateur. La fixture request produit un objet APIRequestContext qui dispose des methodes get, post, put, patch, delete, et fetch pour les cas exotiques. Le baseURL de la configuration est partagé entre page et request, ce qui permet d écrire '/api/health' au lieu de l’URL complete.
Etape 2 — Configurer un projet API-only
Pour ne pas mélanger les tests E2E (qui chargent un navigateur) et les tests d’API (qui n’en chargent pas), on declare deux projects distincts dans playwright.config.ts. Cette séparation évite que les tests d’API attendent inutilement un binaire navigateur.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: process.env.API_URL || 'http://127.0.0.1:8000',
extraHTTPHeaders: {
'Accept': 'application/json',
},
},
projects: [
{
name: 'api',
testDir: './tests/api',
use: {},
},
{
name: 'e2e-chromium',
testDir: './tests/e2e',
use: { browserName: 'chromium' },
},
],
});
Cette configuration donne deux dossiers séparés. tests/api/ contient les *.spec.ts qui n utilisent que la fixture request. tests/e2e/ contient ceux qui utilisent page. Pour ne lancer que les tests d’API, on exécute npx playwright test --project=api, ce qui economise le temps de chargement des navigateurs. Le extraHTTPHeaders garantit que toutes les requetes incluent Accept: application/json, ce qui évite les surprises avec les APIs qui répondent en HTML par défaut.
Etape 3 — Tester un POST avec authentification
La plupart des APIs reelles exigent un token. Le scénario classique : on POST sur /auth/login avec email et mot de passe, on recupere un token JWT, on l utilisé dans les requetes suivantes. Avec Playwright, on encode ce flux dans un fichier de setup exécute une fois au démarrage.
// tests/api/setup.ts
import { request } from '@playwright/test';
export async function getAuthToken(): Promise<string> {
const ctx = await request.newContext();
const r = await ctx.post('/auth/login', {
data: { email: 'test@example.com', password: 'test-password' },
});
if (r.status() !== 200) throw new Error('Login failed: ' + r.status());
const body = await r.json();
await ctx.dispose();
return body.token;
}
La fonction est appelée dans un beforeAll au niveau de la suite, ou mieux, dans un fichier global-setup.ts qui écrit le token dans une variable d environnement. Cette dernière approche est plus propre car le token n’a pas a être passe explicitement entre les fichiers de test.
Etape 4 — Ecrire un test POST avec body et headers
Une fois le token disponible, on teste un endpoint protégé. Imaginons une API de boutique avec POST /api/orders qui cree une commande et retourne l’ID. On envoie le body en JSON, on ajouté le header Authorization, on assert sur le statut et la structuré de la réponse.
// tests/api/orders.spec.ts
import { test, expect } from '@playwright/test';
const TOKEN = process.env.TEST_TOKEN!;
test('POST /api/orders cree une commande et retourne l ID', async ({ request }) => {
const response = await request.post('/api/orders', {
headers: { Authorization: 'Bearer ' + TOKEN },
data: {
items: [
{ sku: 'DATTES-MEDJOOL-1KG', qty: 2 },
{ sku: 'HUILE-OLIVE-500ML', qty: 1 },
],
shipping: 'standard',
},
});
expect(response.status()).toBe(201);
const body = await response.json();
expect(body).toMatchObject({
id: expect.stringMatching(/^ord_/),
status: 'pending',
total: expect.any(Number),
});
});
Le matcher toMatchObject autorise un sous-ensemble : on vérifie que les clés attendues existent et matchent, sans imposer que le body ne contienne que celles-la. C est plus robuste que toEqual, qui echouerait si l’API ajouté un nouveau champ. Les matchers expect.stringMatching et expect.any(Number) permettent de vérifier le format sans bloquer la valeur exacte — l’ID est genere aleatoirement, le total depend du prix actuel.
Etape 5 — Tester les codes d’erreur
Une API testee uniquement sur le chemin heureux est une API qui plantera en production sur le premier cas particulier. La règle de l équipe doit être : pour chaque endpoint, au moins un test du chemin heureux et un test d’erreur typique (400 sur body invalide, 401 sans auth, 404 sur ressource inexistante, 422 sur validation échouée).
test('POST /api/orders refuse un body invalide avec 400', async ({ request }) => {
const response = await request.post('/api/orders', {
headers: { Authorization: 'Bearer ' + TOKEN },
data: { items: [] }, // panier vide invalide
});
expect(response.status()).toBe(400);
const body = await response.json();
expect(body.error).toContain('items');
});
test('POST /api/orders sans auth retourne 401', async ({ request }) => {
const response = await request.post('/api/orders', {
data: { items: [{ sku: 'DATTES-MEDJOOL-1KG', qty: 1 }] },
});
expect(response.status()).toBe(401);
});
Ces deux tests valident des contrats explicites : le serveur doit refuser un panier vide avec 400 et un appel sans token avec 401. Si demain quelqu un casse cette validation cote serveur, la suite le voit. C est exactement l usage des tests de contrat : geler le comportement public et alerter si on devie.
Etape 6 — Reutiliser un contexte avec session persistee
Pour les suites longues, recreer un contexte HTTP a chaque test devient coûteux (TLS handshake, ouverture de socket, parsing). On peut creer un APIRequestContext reutilise au niveau de la suite, qui garde la session ouverte.
import { test as base, APIRequestContext, request } from '@playwright/test';
const test = base.extend<{ api: APIRequestContext }>({
api: async ({}, use) => {
const ctx = await request.newContext({
baseURL: process.env.API_URL,
extraHTTPHeaders: { Authorization: 'Bearer ' + process.env.TEST_TOKEN },
});
await use(ctx);
await ctx.dispose();
},
});
test('liste des produits', async ({ api }) => {
const r = await api.get('/api/products');
expect(r.ok()).toBeTruthy();
expect((await r.json()).length).toBeGreaterThan(0);
});
La fixture personnalisee api cree un contexte avec le token déjà injecte dans les headers, et le ferme proprement après chaque test. Tous les tests qui declarent api en paramètre y ont accès. Cette technique réduit le bruit dans les tests : plus besoin d écrire les headers a chaque appel, plus besoin de gérer l auth manuellement.
Etape 7 — Lancer la suite et lire le rapport
Quand la suite atteint plusieurs dizaines de tests, on l exécute en mode batch et on analyse le rapport HTML.
npx playwright test --project=api
npx playwright show-report
Le rapport HTML liste chaque test avec sa duree, son statut, et le détail des requetes effectuees. Pour les tests d’API qui échouent, le rapport montre la requete envoyée, la réponse reçue, et les headers — ce qui permet souvent de diagnostiquer en quelques secondes une 401 due a un token expire ou une 422 due a un champ obligatoire manquant.
Erreurs fréquentes
| Symptome | Cause probable | Solution |
|---|---|---|
| Tous les tests sortent 401 | Token expire ou non injecte | Regenerer dans le global-setup et passer en variable d environnement |
| Test passe en local, échoue en CI avec ECONNREFUSED | L’API n’a pas fini de démarrer | Configurer webServer dans la config Playwright pour attendre que le port reponde |
| Reponse JSON parsée comme texte | Header Accept manquant |
Ajouter extraHTTPHeaders.Accept = 'application/json' globalement |
| Test hangs sur les uploads de fichiers | Mauvaise serialisation du multipart | Utiliser multipart au lieu de data et passer un Buffer |
| Token traine dans les logs | Verbose mode actif | Masquer la valeur dans la config ou l afficher tronquee |
Tutoriels associes
- Tests E2E avec Playwright en 2026 : tutoriel pas-à-pas — installation, locators, auto-attente et trace viewer.
- Mocks et fixtures avec MSW en 2026 : tutoriel pas-à-pas — handlers REST, intégration Vitest et mode développement.
- CI parallélisée avec GitHub Actions en 2026 : tutoriel pas-à-pas — sharding Playwright, cache des binaires et fusion des rapports.
Bonnes pratiques approfondies
Sur une suite d’API qui depasse cinquante endpoints, plusieurs disciplines deviennent vitales. La première est la séparation entre tests de contrat et tests de comportement. Un test de contrat vérifie qu un endpoint retourne le bon schema (champs présents, types corrects) — ce qui détecté les ruptures rétro-incompatibles. Un test de comportement vérifie qu une operation a bien le bon effet (creer un objet, modifier un état). Les deux ne sont pas redondants : un endpoint peut respecter son contrat tout en faisant n importe quoi.
La deuxieme discipline est l usage de la validation Zod ou Valibot dans les assertions. Au lieu de expect(body).toMatchObject({...}) on parse le body avec un schema. Si le schema échoue, on a une erreur très précise (chemin, type attendu, type reçu). Cette pratique transforme les tests d’API en garde-fous typage qui durent dans le temps : un changement de structuré casse plusieurs tests d’un coup, exactement la ou il faut.
La troisieme discipline est l’isolation des données. Une API testee sur une base partagée va voir des conflits aleatoires. Soit chaque test cree ses données avec un identifiant aleatoire qu’il connait (un uuid en paramètre), soit on ouvre une transaction au debut et on rollback a la fin (possible avec certains frameworks comme Prisma). Ces deux techniques rendent la suite parallelisable sans douleur.
Cas avancés : streaming, file upload, websockets
Les APIs reelles vont au-dela du JSON simple. Pour tester un endpoint qui retourne un flux SSE (Server-Sent Events), on utilisé response.body qui exposé un ReadableStream et on lit les chunks en boucle. Pour un upload de fichier en multipart, on passe multipart: { file: fs.createReadStream('test.pdf') } au lieu de data. Pour un endpoint websocket, Playwright fournit page.waitForEvent('websocket') mais c’est rarement le bon outil — privilegier une bibliothèque cliente WS dédiée dans Vitest pour ce cas précis.
Pour les APIs qui exigent une signature HMAC sur le body (banques, paiements, webhooks), il faut calculer la signature dans une fixture personnalisee qui prend la clé privee depuis une variable d environnement. Ne jamais committer la clé dans le repo, même dans un fichier .env.test. Stocker dans GitHub Actions secrets et l injecter au runtime.
Versionner les tests d’API au rythme du backend
Les APIs évoluent, parfois en cassant la rétro-compatibilité. Les tests doivent suivre. Une discipline a établir tôt dans le projet est de marquer chaque test avec la version d’API qu’il valide. Concretement, on ajouté un commentaire ou un tag @api-v2 dans le describe, ce qui permet de filtrer les tests par version au lancement.
Quand l’API passe de v1 a v2, on garde les deux suites de tests pendant la periode de transition. Les tests v1 protègent les anciens consommateurs jusqu’à leur deprecation effective, les tests v2 valident le nouveau contrat. Cette double protection coûte peu en temps d exécution mais sauve souvent des migrations bacle.
Pour les changements purement additifs (un nouveau champ optionnel dans la réponse), pas besoin de versionner : un test écrit sur v1 continue de passer sur v2 grace a toMatchObject qui accepte les sur-ensembles. C est l avantage d’une assertion intelligente sur une assertion stricte.
Tests de contrat et generation de clients
Pour les équipes qui pratiquent le contract testing, Pact est la référence. L idée : le consommateur publie un contrat decrivant ce qu’il attend, le producteur vérifie qu’il respecte ce contrat. Ce mécanisme détecté les ruptures avant le déploiement, sans avoir besoin que les deux services tournent ensemble.
Une approche plus légère consiste a generer le client TypeScript depuis le schema OpenAPI ou GraphQL, et a utiliser ce client genere dans les tests. Si le serveur change le schema, le client genere change, et les tests ne compilent plus. Le test devient un compilateur, ce qui est l’idéal en termes de feedback rapide. Les outils comme openapi-typescript-codegen et graphql-codegen automatisent cette generation dans la CI.
Ressources officielles
- Documentation API testing Playwright
- Reference APIRequestContext
- Documentation des fixtures personnalisees
Pour la vue d’ensemble strategique sur les tests modernes, voir le guide principal : Tests modernes en JavaScript en 2026.
Questions fréquentes
Pourquoi pas Postman ou Insomnia ?
Pour explorer une API manuellement, ces outils restent excellents. Pour automatiser des centaines de tests dans une CI avec types TypeScript et intégration au reste du code, Playwright est plus efficace : un seul outil, un seul rapport, le même langage que l application.
Comment tester un GraphQL endpoint ?
Avec un POST classique sur /graphql dont le body contient { query: '...', variables: {...} }. Tous les autres principes restent valables.
Faut-il une base de données dédiée ?
Pour les tests d écriture (POST, PUT, DELETE), oui. La pratique recommandee est SQLite en mémoire ou une base Postgres jetable demarree par Docker au debut du job CI, seedee avec des données minimales. Ne jamais tester contre la production.
Doit-on retester les mêmes endpoints en E2E ?
Non. Si l’API est testee unitaire (tests d’API rapides) et que le frontend est teste unitaire avec MSW (mocks d’API), les tests E2E n’ont besoin de couvrir que les parcours metier complets, pas chaque endpoint.
Comment gérer les rate limits sur l’API testee ?
Limiter le parallelisme avec workers: 1 sur les suites concernees, ou prévoir un bypass cote serveur en environnement de test (header X-Test-Mode par exemple).