L'API page.route() de Playwright intercepte chaque requête HTTP que le navigateur envoie et vous laisse choisir : retourner une réponse fictive, bloquer la requête entièrement, ou la modifier à la volée.
Pourquoi mocker les requêtes réseau
La question n'est pas d'éviter les vrais tests. C'est de tester la bonne chose au bon niveau.
Quand un test UI frappe une vraie API, vous êtes soumis à la latence réseau, à l'état du serveur, et à la disponibilité de services tiers. Un test qui vérifie comment un tableau s'affiche quand l'endpoint /api/items retourne un tableau vide est un test frontend. Il ne devrait pas requérir un état de base de données spécifique pour passer. Le mocking découple ce problème entièrement.
Trois raisons principales de mocker :
La vitesse, c'est la plus évidente. Un test qui intercepte les appels réseau et retourne une réponse pré-construite s'exécute en millisecondes au lieu d'attendre un vrai aller-retour.
La fiabilité, c'est la plus importante. Les tests qui frappent de vrais backends échouent pour des raisons sans rapport avec ce que vous testez : l'environnement de staging est hors ligne, une migration a tourné, quelqu'un a supprimé les données de test. Les réponses mockées sont déterministes par définition.
Les états d'erreur, c'est la raison la plus sous-estimée. Vous ne pouvez pas déclencher de façon fiable une 503 ou un timeout réseau contre un vrai serveur dans une suite de tests. Avec page.route(), vous produisez ces conditions à la demande.
page.route() : le schéma d'interception
page.route() prend un schéma d'URL (chaîne, glob ou regex) et une fonction de traitement. Chaque requête correspondante passe par ce traitement avant d'atteindre le réseau.
import { test, expect } from '@playwright/test';
test('intercepte une requête réseau', async ({ page }) => {
await page.route('https://lab.becomeqa.com/api/items', route => {
console.log('Requête interceptée :', route.request().url());
route.continue(); // Laisser passer sans modification
});
await page.goto('https://lab.becomeqa.com');
});Le handler reçoit un objet Route avec quatre méthodes principales. fulfill() retourne une réponse mockée, abort() bloque la requête entièrement, continue() la laisse passer, et fallback() délègue au handler suivant. Vous utiliserez les quatre.
Les patterns glob fonctionnent comme attendu :
// Correspondre à toute requête vers le chemin /api/
await page.route('**/api/**', route => route.continue());
// Correspondre à un endpoint spécifique quelle que soit l'origine
await page.route('**/api/items', route => route.continue());page.route() intercepte uniquement les requêtes faites par cette page spécifique. Pour intercepter les requêtes sur plusieurs pages d'un contexte, utilisez browserContext.route().Retourner des réponses JSON mockées avec fulfill()
route.fulfill() court-circuite la requête et retourne la réponse que vous spécifiez. C'est le cœur du mocking UI.
test('le tableau s\'affiche avec des données API mockées', async ({ page }) => {
const mockItems = [
{ id: '1', destination: 'Tokyo', status: 'planned', notes: 'Saison des cerisiers' },
{ id: '2', destination: 'Lisbonne', status: 'completed', notes: '' },
];
await page.route('**/api/items', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockItems),
});
});
await page.goto('https://lab.becomeqa.com');
// Étapes de connexion omises pour la clarté
await expect(page.getByText('Tokyo')).toBeVisible();
await expect(page.getByText('Lisbonne')).toBeVisible();
});Vous contrôlez chaque partie de la réponse : code de statut, en-têtes, type de contenu, et corps. Si l'application vérifie des en-têtes de réponse (comme Content-Type), incluez-les explicitement.
Pour les payloads mockées plus volumineuses, chargez-les depuis un fichier de fixture JSON :
import { readFileSync } from 'fs';
import path from 'path';
test('le tableau s\'affiche avec des données de fixture', async ({ page }) => {
const fixtureBody = readFileSync(
path.join(__dirname, 'fixtures/items.json'),
'utf-8'
);
await page.route('**/api/items', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: fixtureBody,
});
});
await page.goto('https://lab.becomeqa.com');
await expect(page.getByRole('table')).toBeVisible();
});Cela garde les fichiers de test lisibles quand les données mockées sont complexes. Un vrai enregistrement de paiement ou un profil utilisateur très imbriqué n'ont pas leur place en ligne.
Bloquer des requêtes avec abort()
Parfois, vous voulez vérifier ce qui se passe quand une requête ne peut pas aboutir du tout. Cela couvre une image qui échoue à charger, un script d'analytics tiers qui dépasse le délai, ou un appel API non critique que l'application devrait gérer gracieusement.
test('l\'app affiche l\'état d\'erreur quand l\'API est inaccessible', async ({ page }) => {
await page.route('**/api/items', route => {
route.abort('failed'); // Simule une panne de connexion
});
await page.goto('https://lab.becomeqa.com');
// Connexion, navigation vers la vue des items...
// L'app doit afficher un message d'erreur, pas planter
await expect(page.getByText('Unable to load items')).toBeVisible();
await expect(page.getByRole('table')).not.toBeVisible();
});La méthode abort() accepte un code d'erreur : 'failed' pour une erreur de connexion générique, 'timedout' pour simuler un timeout, 'blockedbyclient' pour simuler un blocage de type adblocker. Utilisez 'timedout' pour tester spécifiquement la gestion des timeouts :
test('affiche le message de timeout après une réponse lente', async ({ page }) => {
await page.route('**/api/items', route => {
route.abort('timedout');
});
await page.goto('https://lab.becomeqa.com');
await expect(page.getByText('Request timed out')).toBeVisible();
});Le blocage est aussi utile pour accélérer les tests en supprimant les requêtes que vous savez non pertinentes. Bloquer les analytics tiers, les CDN de polices ou les scripts de tracking peut faire gagner des secondes sur les suites de tests :
test.beforeEach(async ({ page }) => {
// Bloquer les requêtes analytics — inutiles et lentes
await page.route(/google-analytics\.com|segment\.io/, route => route.abort());
});Modifier des requêtes en vol avec continue()
route.continue() laisse passer la requête vers le serveur mais vous laisse remplacer n'importe quelle partie au préalable : URL, méthode, en-têtes ou corps. Utile pour injecter des en-têtes d'authentification sans modifier le code de l'application, ou pour tester comment le backend gère des combinaisons d'en-têtes spécifiques.
test('injecte un en-tête d\'auth dans chaque requête API', async ({ page }) => {
await page.route('**/api/**', route => {
route.continue({
headers: {
...route.request().headers(),
'Authorization': 'Bearer test-token-for-e2e',
},
});
});
await page.goto('https://lab.becomeqa.com/dashboard');
await expect(page.getByRole('table')).toBeVisible();
});Vous pouvez aussi réécrire l'URL de la requête, pratique pour rediriger des appels API de production vers un environnement de staging sans changer aucune config :
test('redirige les appels API vers le staging', async ({ page }) => {
await page.route('https://api.production.com/**', route => {
const newUrl = route.request().url().replace(
'api.production.com',
'api.staging.becomeqa.com'
);
route.continue({ url: newUrl });
});
await page.goto('https://lab.becomeqa.com');
});continue() et des en-têtes personnalisés, étendez toujours route.request().headers() en premier. Remplacer les en-têtes entièrement supprimera des choses comme Content-Type et Accept que le serveur peut nécessiter.Inspecter avec waitForRequest et waitForResponse
Parfois, le but n'est pas de mocker quoi que ce soit. C'est de vérifier qu'une requête spécifique a bien eu lieu, ou de capturer les données de réponse pour assertion. page.waitForRequest() et page.waitForResponse() retournent des promises qui se résolvent quand une requête ou réponse correspondante est détectée.
test('cliquer sur Enregistrer envoie le bon payload', async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
// Étapes de connexion...
// Configurer l'écouteur AVANT de déclencher l'action
const requestPromise = page.waitForRequest(request =>
request.url().includes('/api/items') && request.method() === 'POST'
);
await page.getByRole('button', { name: 'Add Item' }).click();
await page.getByLabel('Destination').fill('Berlin');
await page.getByRole('button', { name: 'Save' }).click();
const request = await requestPromise;
const payload = request.postDataJSON();
expect(payload.destination).toBe('Berlin');
expect(payload.status).toBeDefined();
});Le détail critique : configurez l'écouteur avant de déclencher l'action. Si vous attendez d'abord le clic puis attendez waitForRequest, la requête a peut-être déjà été envoyée et vous attendrez indéfiniment.
waitForResponse fonctionne pareil, mais se résout avec la réponse :
test('le formulaire de paiement affiche le message de succès après confirmation API', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/payment');
const responsePromise = page.waitForResponse(response =>
response.url().includes('/api/payments') && response.status() === 200
);
await page.getByLabel('Card Number').fill('4111111111111111');
await page.getByRole('button', { name: 'Pay' }).click();
const response = await responsePromise;
const body = await response.json();
expect(body.status).toBe('success');
await expect(page.getByText('Payment confirmed')).toBeVisible();
});Tester les états d'erreur : 500, 401 et timeouts
Les tests d'états d'erreur, c'est là que page.route() justifie vraiment sa place. Ces scénarios sont quasi impossibles à déclencher de façon fiable contre un vrai backend, mais faciles à mocker.
test('affiche la bannière d\'erreur en cas de défaillance serveur', async ({ page }) => {
await page.route('**/api/items', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal server error' }),
});
});
await page.goto('https://lab.becomeqa.com');
// Connexion...
await expect(page.getByRole('alert')).toContainText('Something went wrong');
});test('redirige vers la connexion quand la session expire', async ({ page }) => {
let requestCount = 0;
await page.route('**/api/items', route => {
requestCount++;
if (requestCount === 1) {
// La première requête réussit — l'utilisateur est "connecté"
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: '1', destination: 'Madrid', status: 'planned' }]),
});
} else {
// Les requêtes suivantes retournent 401 — session expirée
route.fulfill({
status: 401,
contentType: 'application/json',
body: JSON.stringify({ error: 'Session expired' }),
});
}
});
await page.goto('https://lab.becomeqa.com');
// Vérifier que le chargement initial fonctionne, puis déclencher un rechargement...
await page.reload();
await expect(page).toHaveURL(/\/login/);
});test('affiche le bouton de retry après un timeout réseau', async ({ page }) => {
await page.route('**/api/items', async route => {
// Délai puis abandon — simule un réseau lent qui dépasse le délai
await new Promise(resolve => setTimeout(resolve, 8000));
route.abort('timedout');
});
await page.goto('https://lab.becomeqa.com');
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible({
timeout: 15000,
});
});route.fallback() pour le mocking partiel
route.fallback() permet à un handler de se retirer et de laisser le handler suivant (ou le vrai réseau) prendre le relais. C'est l'outil approprié quand vous voulez mocker des endpoints spécifiques tout en laissant tout le reste atteindre le vrai serveur.
test('mocke uniquement l\'endpoint de paiement', async ({ page }) => {
// Premier handler : mocker l'endpoint de paiement
await page.route('**/api/payments', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ status: 'success', transactionId: 'mock-txn-001' }),
});
});
// Deuxième handler : laisser passer tout le reste
await page.route('**', route => route.fallback());
await page.goto('https://lab.becomeqa.com');
// Vraie connexion, vrai chargement des données — seul le paiement est mocké
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
await page.goto('https://lab.becomeqa.com/payment');
await page.getByRole('button', { name: 'Pay' }).click();
await expect(page.getByText('Payment confirmed')).toBeVisible();
});route.fallback() distingue le mocking chirurgical du mocking tout-ou-rien. Vous pouvez remplacer une dépendance externe fragile en laissant le reste du test ancré dans le comportement réel.
Les handlers multiples pour le même schéma sont évalués dans l'ordre inverse d'enregistrement. Le dernier enregistré est évalué en premier. Quand un handler appelle fallback(), Playwright passe au handler précédemment enregistré.
// Enregistré en premier — agit comme comportement par défaut
await page.route('**/api/**', route => route.continue());
// Enregistré en second — évalué en premier
await page.route('**/api/payments', route => {
route.fulfill({ status: 200, body: JSON.stringify({ status: 'success' }) });
});Quand NE PAS mocker
Le mocking a un coût : vos tests sont aussi bons que vos données mockées. Si la vraie API retourne une structure que vous n'aviez pas anticipée dans votre fixture, vos tests mockés passeront pendant que la production casse.
Certaines catégories de tests sont directement pénalisées par le mocking.
Les tests de contrat doivent frapper la vraie API. Si vous vérifiez qu'une requête frontend correspond au contrat attendu du backend (noms de champs, en-têtes requis, forme de la réponse), un mock ne peut pas détecter la divergence. C'est exactement pour ça que la vraie API existe. Les tests d'intégration pour les flux critiques devraient utiliser le vrai backend. Le flux de connexion, le flux de paiement, la soumission de données qui pilote le cœur du métier ont besoin d'une vraie intégration pour détecter les vraies défaillances. Mockez-les et vous testez la confiance, pas le comportement. Quand vous déboguez un vrai bug, le mocking vous empêche de voir le vrai problème. Si les utilisateurs signalent un problème sur la page de confirmation de paiement, le dernier endroit où vous voulez un endpoint de paiement mocké qui cache la vraie réponse.La règle pratique : mockez pour rendre un test déterministe et rapide quand la requête réseau n'est pas ce que vous testez. Ne mockez pas quand la requête elle-même, ou le serveur qui la traite, est sous test. Une suite de tests complète utilise les deux. De vrais appels API pour les tests d'intégration et les contrats, des réponses mockées pour les tests de rendu UI et les états d'erreur.
FAQ
page.route() affecte-t-il les requêtes qui démarrent avant l'enregistrement du handler ?
Non. Les handlers interceptent uniquement les requêtes initiées après leur enregistrement. Appelez toujours page.route() avant page.goto() ou avant l'action qui déclenche la requête.
Oui. Playwright évalue les handlers dans l'ordre inverse d'enregistrement. Utilisez route.fallback() pour passer le contrôle au handler suivant. Utilisez page.unroute() pour supprimer un handler quand vous n'en avez plus besoin.
route.fulfill() ne supporte pas le streaming nativement. Il envoie le corps en entier d'un coup. Pour les scénarios de streaming, vous aurez besoin d'un serveur de test local ou d'un outil comme msw (Mock Service Worker) intégré avec Playwright.
Les données mockées doivent-elles vivre dans les fichiers de test ou les fichiers de fixture ?
Les objets mock courts (2 à 3 champs) sont bien en inline. Tout ce qui est plus volumineux ou réutilisé entre tests appartient à un répertoire fixtures/. Cela garde les fichiers de test focalisés sur le comportement, pas sur la configuration des données.
page.route() et les Service Workers pour le mocking ?
page.route() intercepte au niveau Playwright, avant la pile réseau du navigateur. Les Service Workers interceptent à l'intérieur du navigateur. Pour les tests Playwright, page.route() est plus simple, plus fiable, et ne requiert pas de configuration dans le code de l'application. Les Service Workers sont utiles quand vous avez besoin que le mock persiste à travers des navigations de page complètes ou affecte le cache du service worker.
→ See also: Tests d'API avec Playwright: Au-delà de l'Interface | Construire un Framework de Tests Playwright Évolutif de Zéro | Tests d'API avec l'APIRequestContext de Playwright (Sans Postman)