End-to-End Testing

Tests de bout en bout (End-to-End)

Les tests de bout en bout pilotent le système réel comme le ferait un utilisateur. Ils sont lents, peu nombreux et irremplaçables : ce sont les seuls tests capables de prouver qu'une personne peut réellement accomplir une tâche.

intermediate15 min readUpdated 16 sept. 2026
checkout.spec.ts
ts
// tests/checkout.spec.ts
import { test, expect } from "@playwright/test";

test("a signed-in user can buy a plan", async ({ page }) => {
  await page.goto("/pricing");

  await page.getByRole("button", { name: "Choose Pro" }).click();
  await page.getByLabel("Card number").fill("4242 4242 4242 4242");
  await page.getByRole("button", { name: "Pay" }).click();

  await expect(
    page.getByRole("heading", { name: "You're on Pro" }),
  ).toBeVisible();
});
Périmètre
L'ensemble du système
Vitesse
Secondes par test
Quantité
Peu nombreux, haute valeur
Outils courants
Playwright, Cypress
S'exécute dans
Un vrai navigateur
Couvre
Les parcours utilisateurs critiques

Pourquoi c'est important

Ce que vous apporte le test e2e

Confiance dans tout le parcours

Un test e2e traverse l'UI, l'API, la base de données et revient, c'est donc le seul test capable de prouver qu'un utilisateur peut réellement terminer une tâche.

Un filet de sécurité contre les régressions

Une poignée de tests de parcours capturent les ruptures d'intégration que les tests unitaires manquent : un champ renommé, une redirection cassée, une migration manquante.

Une documentation exécutable

Une spécification lisible décrit le produit dans le langage de l'utilisateur et, contrairement à une page wiki, elle fait échouer le build lorsqu'elle devient obsolète.

Le tableau complet

Les trois couches d'un test e2e

Initialiser l'environnement, piloter le parcours utilisateur et affirmer ce qu'une personne verrait.

La stack

Arrange

Une vraie base de données, une API et un frontend, initialisés dans un état connu. Si l'environnement n'est pas reproductible, aucune assertion ne peut être fiable.

Le parcours

Act

Piloter l'interface comme le ferait une personne, via les rôles, les labels et le texte visible plutôt que par des détails d'implémentation.

Le résultat

Assert

Vérifier ce que l'utilisateur voit et, lorsque c'est important, confirmer l'effet de bord via l'API ou la base de données.

HTML5 en un coup d'oeil

La boîte à outils e2e

Naviguer

page.goto charge une route et le test commence à partir d'une URL réelle.

Localiser

getByRole, getByLabel et getByText trouvent les éléments comme le ferait un utilisateur.

Agir

click, fill, type et select pilotent l'interface.

Affirmer

expect(locator).toBeVisible() réessaie jusqu'à ce que le résultat soit vrai.

Authentifier

Réutiliser un storageState sauvegardé au lieu de se connecter à chaque test.

Observer

Attendre des réponses, bloquer des tiers et affirmer sur des appels API.

Flux

Le cheminement derrière chaque test réussi

Un bon test e2e prépare le monde, effectue un seul parcours et laisse l'environnement tel qu'il l'a trouvé.

  1. 1

    Initialiser les données via l'API

    Créez l'utilisateur, le plan et tous les enregistrements dont le parcours a besoin via des appels API, et non en cliquant sur des écrans de configuration.

  2. 2

    Ouvrir l'application

    Naviguez vers la route d'entrée avec une session authentifiée déjà en place pour que le test commence là où l'utilisateur commence.

  3. 3

    Effectuer l'action

    Pilotez l'interface via des locateurs de rôles et de labels, une étape utilisateur à la fois, sans pauses de temps arbitraires.

  4. 4

    Affirmer le résultat visible

    Vérifiez l'élément que l'utilisateur est venu voir. Lorsque le résultat est un effet de bord, confirmez-le également via l'API.

  5. 5

    Capturer des preuves en cas d'échec

    Conservez les traces, captures d'écran et vidéos afin qu'un build rouge en CI puisse être diagnostiqué sans reproduction locale.

  6. 6

    Réinitialiser l'état

    Supprimez ou expirez ce que le test a créé, ou exécutez chaque test avec des données isolées pour que l'ordre n'ait jamais d'importance.

Un bref aperçu

L'évolution des tests navigateur

  1. 2006

    L'arrivée de Selenium

    L'automatisation du navigateur devient possible, et les tests de bout en bout deviennent une véritable pratique avec une courbe d'apprentissage abrupte.

    06
  2. 2015

    Fondation de Cypress

    Un runner orienté développeur place le navigateur dans le même processus et facilite grandement le débogage.

    15
  3. 2020

    Sortie de Playwright

    Une bibliothèque cross-browser avec auto-waiting redéfinit les attentes sur la stabilité des tests navigateur.

    20
  4. 2021

    Playwright Test

    Un runner officiel ajoute des fixtures, le parallélisme, le tracing et le sharding à la bibliothèque.

    21
  5. Aujourd'hui

    Un filtre de qualité ciblé

    Les équipes gardent des suites e2e restreintes et à haute valeur, les exécutant sur les pull requests et avant la mise en production.

    Aujourd'hui

Le guide complet

Tests de bout en bout (End-to-End): Tout ce que vous devez savoir

Ce que signifie réellement le test de bout en bout

Le test de bout en bout (e2e) pilote le système réel du point de vue de l’utilisateur. Au lieu d’appeler une fonction ou une route de manière isolée, un test e2e ouvre l’application, effectue les mêmes étapes qu’une personne réelle et vérifie que le résultat attendu par l’utilisateur s’est bien produit.

Cette définition a deux conséquences. Premièrement, le test traverse toutes les couches du système : navigateur, code frontend, HTTP, API, base de données et retour. Deuxièmement, les assertions portent sur le comportement et non sur l’implémentation. Vous vérifiez qu’une confirmation est visible, et non qu’une fonction particulière a été appelée.

Comme l’ensemble de la stack est sollicité, les tests e2e sont ce qui se rapproche le plus d’une garantie que l’utilisateur peut mener à bien une tâche. Ce sont également les tests les plus lents et les plus coûteux que vous écrirez, c’est précisément pour cela qu’il faut en écrire peu et les choisir avec soin.

Où se situent les tests e2e dans la pyramide des tests

La pyramide des tests est une règle empirique qui définit la proportion de chaque type de test à rédiger. La base est large : de nombreux tests unitaires rapides. Le milieu est plus étroit : des tests d’intégration couvrant quelques composants. Le sommet est réduit : une poignée de parcours de bout en bout (end-to-end).

  • Les tests unitaires s’exécutent en quelques millisecondes, sans infrastructure, et permettent de verrouiller la logique pure et les cas limites.
  • Les tests d’intégration s’exécutent en quelques secondes, sollicitent une route ou un module avec de vrais collaborateurs, et détectent les bugs de câblage.
  • Les tests end-to-end s’exécutent en plusieurs dizaines de secondes, tournent sur une stack déployée, et prouvent qu’un utilisateur peut accomplir une tâche critique.

Cette forme est importante car le coût augmente et le feedback ralentit à mesure que l’on monte dans la pyramide. Un bug qu’un test unitaire peut détecter doit être trouvé par un test unitaire. Réservez les tests e2e pour les parcours où un chemin rompu signifie un produit cassé : inscription, connexion, paiement, publication, invitation. Si un test n’a pas besoin d’un navigateur pour être pertinent, il ne devrait probablement pas en utiliser un.

Un test de cohérence utile : pour chaque test e2e, demandez-vous quel test unitaire ou d’intégration aurait pu détecter le même bug. Si la réponse est « un test facile à écrire », alors le test e2e se trouve dans la mauvaise couche.

Choisir les parcours qui valent la peine d’être automatisés

Vous ne pouvez pas tout automatiser. Choisissez donc les parcours où un échec coûte le plus cher et où un bug est le plus susceptible de passer à travers les couches de tests inférieures.

Évaluez chaque parcours candidat selon deux axes : l’impact business et le risque d’intégration. C’est là où l’impact est élevé et le risque important que les tests e2e justifient pleinement leur coût.

  • Impact élevé, risque élevé — inscription, connexion, paiement, réinitialisation du mot de passe. Automatisez-les en priorité.
  • Impact élevé, risque faible — la page d’accueil marketing. Couvrez-la avec un smoke test, pas avec un parcours complet.
  • Impact faible, risque élevé — un export administrateur utilisé mensuellement. Un seul test suffit, ou une vérification scriptée.
  • Impact faible, risque faible — les commutateurs de paramètres et les états cosmétiques. Laissez cela aux tests unitaires et aux tests de composants.

Les parcours qui traversent le plus de frontières sont les plus précieux, car ce sont ceux qu’un test unitaire ne peut pas atteindre. Un processus de paiement sollicite le panier, la tarification, le paiement, le service de commande et l’e-mail — précisément les connexions qui cassent silencieusement lorsqu’une équipe renomme un champ.

Notez cette liste et revoyez-la chaque trimestre. L’ensemble des parcours critiques évolue à mesure que le produit grandit, et le flux important d’hier peut devenir le code mort d’aujourd’hui.

Choisir son outil : Playwright ou Cypress

Deux outils dominent aujourd’hui les tests de navigateur, et les deux sont excellents.

Playwright pilote Chromium, Firefox et WebKit via une seule API. Par défaut, il exécute les tests en parallèle via des processus workers, supporte plusieurs projets de navigateurs, propose des locators avec auto-attente et inclut un trace viewer qui enregistre l’exécution image par image. Son storageState simplifie la réutilisation de l’authentification, et sa fixture request vous fournit un client API au sein du même test. C’est aujourd’hui le choix par défaut le plus courant pour les nouvelles suites de tests.

Cypress s’exécute directement dans la boucle d’événements (event loop) du navigateur, ce qui rend le débogage instantané : vous pouvez inspecter le DOM à tout moment et naviguer dans le temps à travers les commandes via le runner. Son API est conviviale et ses messages d’erreur sont excellents. Le compromis réside dans le support multi-navigateurs et le parallélisme, qui ont historiquement nécessité plus de configuration.

Le choix de l’outil importe rarement autant que la discipline appliquée. Une suite Playwright bien écrite et une suite Cypress bien écrite fonctionnent toutes les deux. Choisissez-en un, apprenez son modèle d’attente, et concentrez vos efforts sur l’isolation et les locators.

L’anatomie d’un test

Presque tous les tests e2e sont composés des trois mêmes étapes, généralement appelées arrange, act et assert.

test("a signed-in user can upgrade to Pro", async ({ page }) => {
  // arrange: the account and plan already exist
  // act: perform the journey
  await page.goto("/pricing");
  await page.getByRole("button", { name: "Choose Pro" }).click();

  // assert: the user sees the result
  await expect(
    page.getByRole("heading", { name: "You're on Pro" }),
  ).toBeVisible();
});

Dans la mesure du possible, gardez l’étape arrange en dehors de l’interface utilisateur. Créer un utilisateur en remplissant un formulaire d’inscription rallonge la durée d’exécution de votre suite de tests et couple chaque test au flux d’inscription. Créez-le plutôt via l’API, puis commencez le test en étant déjà connecté.

Une autre bonne habitude à prendre est de limiter chaque test à un seul parcours. Lorsqu’un test couvre cinq éléments et échoue, vous savez seulement que l’un de ces cinq éléments est cassé. Lorsqu’il n’en couvre qu’un seul, le nom du test en échec constitue directement le diagnostic.

Rédiger un parcours fluide et lisible

Un test e2e est lu bien plus souvent qu’il n’est écrit. Un lecteur doit être capable de comprendre ce qu’il protège sans avoir besoin d’ouvrir l’application.

Nommez le test en fonction du résultat pour l’utilisateur, et non de l’implémentation. « un utilisateur connecté peut passer à l’offre Pro » est préférable à « test clic bouton paiement ». Utilisez le présent et le vocabulaire de l’utilisateur.

Structurez le test comme une séquence d’actions utilisateur, une par ligne, avec des lignes vides entre les différentes phases.

test("an admin can invite a teammate", async ({ page }) => {
  await page.goto("/team");

  await page.getByRole("button", { name: "Invite" }).click();
  await page.getByLabel("Email").fill("[email protected]");
  await page.getByRole("button", { name: "Send invite" }).click();

  await expect(page.getByText("Invitation sent")).toBeVisible();
});

Extrayez les parcours répétitifs dans des helpers, tout en veillant à ce que le test reste lisible. Un helper nommé upgradeToPro(page) est une bonne pratique ; un helper qui masque l’intégralité du test ne l’est pas. Le test doit toujours laisser apparaître les étapes.

Enfin, commentez le « pourquoi » et non le « quoi ». Le code indique déjà quel bouton a été cliqué ; un commentaire n’est utile que s’il explique pourquoi le test existe ou pourquoi une étape semble inhabituelle.

Locators : adressez-vous à l’utilisateur, pas au DOM

Un locator est la méthode utilisée par le test pour trouver un élément. L’amélioration la plus significative que vous puissiez apporter à une suite e2e est de choisir vos locators de la même manière qu’un utilisateur le ferait.

Playwright les classe du plus recommandé au moins recommandé :

  1. getByRole — boutons, liens, titres et champs de saisie par rôle accessible et nom.
  2. getByLabel — contrôles de formulaire par leur label.
  3. getByText — texte visible.
  4. getByPlaceholder, getByAltText, getByTitle — autres attributs visibles.
  5. getByTestId — un test id stable lorsque rien d’autre orienté utilisateur ne convient.
await page.getByRole("button", { name: "Add to cart" }).click();
await page.getByLabel("Email").fill("[email protected]");
await expect(page.getByRole("heading", { name: "Your cart" })).toBeVisible();

Cet ordre n’est pas esthétique. Les locators basés sur le rôle et le label échouent lorsqu’un élément est inaccessible, le test faisant ainsi office de vérification d’accessibilité. Ils résistent également aux refactorisations : renommer une classe CSS ne les impacte pas, alors que cela casserait un .btn.btn--primary > span.

Utilisez getByTestId de manière délibérée, et non par défaut. Un test id est un contrat que vous devez maintenir, et une suite remplie de test ids ne vous indique en rien si l’interface est cohérente.

Gérer l’authentification

Se connecter via l’interface utilisateur avant chaque test est la cause principale de suites e2e lentes et instables. Une connexion implique un formulaire, un aller-retour réseau et une redirection, répétés des centaines de fois.

La solution consiste à s’authentifier une seule fois et à réutiliser la session. Playwright appelle cette session sauvegardée storageState — il s’agit des cookies et du local storage sérialisés dans un fichier.

// tests/auth.setup.ts
import { test as setup, expect } from "@playwright/test";

setup("authenticate", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("Email").fill(process.env.E2E_EMAIL!);
  await page.getByLabel("Password").fill(process.env.E2E_PASSWORD!);
  await page.getByRole("button", { name: "Sign in" }).click();

  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
  await page.context().storageState({ path: "playwright/.auth/user.json" });
});

Un projet de configuration (setup project) écrit le fichier, et les projets de navigateur en dépendent et le chargent via use.storageState. Les tests nécessitant un utilisateur différent utilisent un fichier d’état distinct.

// playwright.config.ts
export default defineConfig({
  projects: [
    { name: "setup", testMatch: /auth\.setup\.ts/ },
    {
      name: "chromium",
      use: { storageState: "playwright/.auth/user.json" },
      dependencies: ["setup"],
    },
  ],
});

Vous devriez tout de même conserver un test qui effectue la connexion via l’interface. Le formulaire de connexion est un parcours utilisateur à part entière, et le raccourci ne doit jamais être l’unique moyen de le tester.

Rendre les tests déterministes

Le déterminisme est la clé. Un test qui réussit par intermittence est pire qu’un test qui échoue systématiquement, car il apprend à l’équipe à ignorer les alertes.

Trois règles permettent de couvrir la majorité des cas.

Ne jamais utiliser sleep. waitForTimeout(3000) est soit trop court et instable, soit trop long et lent. Attendez plutôt que la condition soit remplie : await expect(locator).toBeVisible(). Les assertions conçues pour le web réessaient jusqu’à ce qu’elles réussissent ou qu’un délai d’expiration soit atteint, ce qui est exactement le comportement recherché.

Contrôler le réseau. Simulez les tiers et les dépendances instables avec page.route, afin qu’un environnement de test de paiement lent ne fasse pas échouer votre suite de tests. Effectuez des assertions sur les requêtes émises par votre application lorsque c’est le comportement testé.

await page.route("**/api/recommendations", (route) =>
  route.fulfill({ json: { items: [] } }),
);

Figer le temps et le hasard. Un test qui dépend de la date du jour ou d’un identifiant aléatoire échouera tôt ou tard. Injectez une horloge ou fixez les valeurs lues par le test.

Il y a également le navigateur lui-même : les animations, l’autofocus et les transitions créent des conditions de concurrence (race conditions) qui ressemblent à des bugs applicatifs. Désactivez les animations dans l’environnement de test quand c’est possible, et privilégiez les assertions sur l’état final.

Couverture multi-navigateurs et responsive

Un parcours qui fonctionne sous Chromium peut échouer sous WebKit, et une mise en page adaptée à un ordinateur portable peut s’avérer inutilisable sur un téléphone. La couverture multi-navigateurs et multi-viewports est l’un des rares aspects où les tests e2e apportent une valeur ajoutée irremplaçable.

Playwright transforme cela en un simple problème de configuration. Définissez un projet par navigateur et réutilisez les mêmes tests.

// playwright.config.ts
export default defineConfig({
  projects: [
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
    { name: "firefox", use: { ...devices["Desktop Firefox"] } },
    { name: "webkit", use: { ...devices["Desktop Safari"] } },
    { name: "mobile", use: { ...devices["iPhone 13"] } },
  ],
});

Tous les tests n’ont pas besoin d’être exécutés sur tous les navigateurs. Lancez la suite complète sur Chromium pour un feedback rapide, et exécutez les parcours critiques sur l’ensemble des projets selon un planning précis ou avant chaque mise en production. Cela permet de garder des pull requests rapides sans sacrifier la couverture.

Lorsqu’un bug est spécifique à un navigateur, ajoutez un test de régression dans le projet concerné. Un commentaire incluant un lien vers le ticket d’incident a bien plus de valeur qu’une note vague du type « Safari est bizarre ».

Gérer les données de test

Les données sont la raison pour laquelle les suites e2e deviennent discrètement instables. Deux tests qui créent tous deux un utilisateur nommé [email protected] réussiront s’ils sont lancés séparément, mais échoueront s’ils sont exécutés ensemble.

Effectuez le seed via l’API. C’est plus rapide que via l’UI, cela signale clairement les erreurs lorsque le seeding échoue, et cela permet de garder le test concentré sur le parcours utilisateur plutôt que sur la configuration.

const res = await request.post("/api/test/users", {
  data: { email: `ada+${Date.now()}@example.com` },
});
expect(res.ok()).toBeTruthy();
const user = await res.json();

Isolez les données par test ou par worker. L’utilisation d’emails uniques, d’enregistrements limités à un tenant ou d’un namespace frais par worker fonctionne parfaitement. L’important est qu’aucun test ne dépende de la même ligne en base de données.

Nettoyez ce que vous créez, ou rendez le nettoyage inutile en limitant la portée des données à l’exécution d’un test et en supprimant l’ensemble du scope à la fin. Les enregistrements résiduels s’accumulent, ralentissent la base de données et finissent par provoquer des échecs qui n’ont aucun lien avec la modification actuelle.

Exécuter les tests sur une stack réelle

Un test e2e a besoin d’un environnement pour s’exécuter : un frontend, une API et une base de données, tous utilisant la même version du code. L’option la plus reproductible est une stack docker compose qui lance l’ensemble avec une seule commande.

docker compose -f docker-compose.e2e.yml up -d --wait
pnpm exec playwright test
docker compose -f docker-compose.e2e.yml down -v

Le flag --wait est important : il force Compose à ne s’arrêter que lorsque les health checks sont validés, ce qui élimine la race condition du type “connexion refusée lors du premier test”. Le -v lors de l’arrêt supprime les volumes afin que l’exécution suivante démarre sur une base propre.

Les environnements de preview vont encore plus loin. Une plateforme build la branche, la déploie sur une URL temporaire et l’expose au job de test. C’est ce qui se rapproche le plus de la production pour un run e2e, et cela permet aux équipes produit et QA de naviguer sur le même build que celui utilisé par les tests.

Pointez votre suite de tests vers l’environnement via une variable d’environnement — BASE_URL — et ne codez jamais l’hôte en dur. La même suite doit pouvoir s’exécuter localement, en CI et sur un environnement de preview.

Quand le parcours est une API, et non un navigateur

Tous les parcours de bout en bout n’ont pas besoin d’un navigateur. Un webhook, un job d’arrière-plan, une CLI ou un flux de service à service sont tous des parcours de bout en bout au sens où cela compte : ils sollicitent le système réel.

La fixture request de Playwright est un client API complet, donc un parcours API ressemble presque exactement à un parcours via navigateur.

test("a paid order is fulfilled end to end", async ({ request }) => {
  const order = await request.post("/api/orders", {
    data: { sku: "pro-plan", quantity: 1 },
  });
  expect(order.status()).toBe(201);

  await request.post("/api/payments/webhook", {
    data: { orderId: (await order.json()).id, status: "paid" },
  });

  const res = await request.get(`/api/orders/${(await order.json()).id}`);
  expect(await res.json()).toMatchObject({ status: "fulfilled" });
});

Ces tests sont plus rapides et beaucoup moins instables (“flaky”) que les tests de navigateur ; utilisez-les donc pour les flux qui n’ont réellement pas besoin d’une interface utilisateur. Pour une couverture API pure à un niveau plus bas, Supertest est l’outil le plus léger.

Exécuter les tests e2e en CI

Les tests E2E doivent être lancés lors des pull requests et avant une release, et non à chaque sauvegarde. En CI, quelques flags font la différence entre un garde-fou utile et un processus ignoré.

  • Exécuter en mode headless. Il n’y a pas d’affichage ; Playwright et Cypress utilisent tous deux le mode headless par défaut en CI.
  • Sharder la suite de tests. Répartissez les fichiers sur plusieurs machines avec --shard=1/3 pour que le temps d’exécution réel reste constant à mesure que la suite s’agrandit.
  • Relancer une seule fois. Un seul retry permet de distinguer un échec réel d’un problème d’infrastructure, à condition de suivre le taux de retry plutôt que de le laisser masquer l’instabilité (flakiness).
  • Uploader les artefacts en cas d’échec. Les traces, captures d’écran et vidéos sont les seuls moyens de diagnostiquer un build rouge en CI sans avoir à le reproduire localement.
- run: pnpm exec playwright test --shard=${{ matrix.shard }}/3 --retries=1
- uses: actions/upload-artifact@v4
  if: ${{ !cancelled() }}
  with:
    name: report-${{ matrix.shard }}
    path: playwright-report/

Les retries sont un filet de sécurité, pas une solution. Si un test nécessite un retry pour passer systématiquement, c’est qu’il présente une véritable race condition ou un bug d’isolation ; le retry ne fait que vous donner du temps pour le trouver.

Déboguer un run en échec

La première question après un échec est toujours la même : qu’est-ce que le navigateur a réellement vu ? Un outil capable d’y répondre sans reproduction locale transforme une investigation de deux heures en une investigation de deux minutes.

Playwright enregistre une trace — captures d’écran, snapshots du DOM, réseau et console — et le trace viewer permet de rejouer le run étape par étape. Conservez les traces en cas d’échec, puis ouvrez le rapport et naviguez jusqu’au moment où l’assertion a échoué.

pnpm exec playwright test --trace on-first-retry
pnpm exec playwright show-trace test-results/checkout/trace.zip

Localement, lancez la suite en mode headed ou en mode UI pour observer le test et le mettre en pause. Cypress propose le même concept via son runner interactif, qui conserve un journal de commandes permettant de voyager dans le temps.

pnpm exec playwright test --ui
pnpm exec playwright test --headed --debug

L’habitude qui rend le débogage rapide consiste à effectuer l’assertion sur l’élément qui pose réellement problème. Un échec à expect(page).toHaveURL(/\/checkout/) vous indique que la navigation n’a jamais eu lieu, ce qui mène à une investigation très différente d’un échec sur le titre de confirmation.

Lutter contre le flakiness

Le flakiness (instabilité des tests) est la taxe à payer pour les tests e2e, et il provient presque toujours de l’une des causes suivantes :

  • Un await manquant ou une race condition. Le test a agi avant que l’application ne soit stabilisée. Corrigez cela avec une assertion web-first, et non avec un sleep.
  • Des données partagées. Deux tests ont modifié la même ligne. Isolez les données par test ou par worker.
  • Une dépendance externe. Un script ou une API tierce était lent. Mockez-le ou excluez-le du chemin critique.
  • Un locator fragile. Le test était lié à un balisage qui a changé. Passez à un locator basé sur un rôle ou un label.
  • Une animation. L’élément était présent mais pas encore stable. Désactivez les animations dans l’environnement de test.

La meilleure façon de combattre le flakiness est de traiter chaque test instable comme un bug avec une cause précise : mettez-le en quarantaine et corrigez la source du problème. Une suite de tests qui passe au vert uniquement grâce aux tentatives de retry est une suite en laquelle personne n’a confiance, et une suite en laquelle personne n’a confiance est pire que l’absence totale de tests.

Maintenir la santé de votre suite de tests dans le temps

Une suite e2e a naturellement tendance à s’étendre jusqu’à devenir lente et passer au rouge. La maintenir en bonne santé est une pratique continue, et non une configuration ponctuelle.

Passez en revue votre suite de tests comme vous passez en revue votre produit. Lorsqu’une fonctionnalité est supprimée, supprimez son test dans la même pull request. Un test qui ne protège plus rien n’est qu’un coût inutile.

Suivez les indicateurs qui comptent : le temps d’exécution total, le taux de réussite et le taux de retry. Une augmentation du taux de retry est le premier signal que le flakiness s’installe, bien avant que la suite ne devienne visiblement instable.

Assumez la responsabilité de la suite. Une suite e2e partagée sans responsable finit par dériver : les échecs sont relancés, les tests sont ignorés, et en l’espace d’un trimestre, plus personne ne se fie à un run rouge. Mettez en place une rotation ou désignez un petit groupe pour la maintenir au vert, et faites de la correction d’un test flaky une tâche prioritaire plutôt qu’une simple interruption.

Gardez la suite suffisamment rapide pour être exécutée à chaque pull request. Lorsqu’elle devient trop volumineuse, utilisez le sharding, parallélisez-la, ou déplacez les parcours les moins critiques vers un run nocturne. Un quality gate qui prend une heure est un quality gate que les développeurs chercheront à contourner.

Ce qu’il ne faut pas couvrir avec les tests e2e

La tentation est de tout tester via le navigateur car cela semble être le scénario le plus réel. Résistez. L’e2e est la couche la plus coûteuse ; utilisez-la donc uniquement là où elle apporte une réelle valeur ajoutée.

Ne couvrez pas avec l’e2e :

  • La logique pure et les cas limites. Le formatage des dates, le calcul des prix, les règles de validation — tout cela relève des tests unitaires qui s’exécutent en quelques millisecondes.
  • Chaque chemin d’erreur. Une page 500 justifie un seul parcours ; les vingt manières dont l’API peut échouer relèvent des tests d’intégration.
  • Les combinaisons d’entrées exhaustives. L’e2e couvre le parcours représentatif, pas la matrice complète.
  • Le comportement des composants. L’ouverture d’un menu déroulant relève d’un test de composant, qui est plus rapide et plus précis.
  • Tout ce qui est déjà couvert en dessous. Dupliquer un test unitaire au niveau de la couche e2e augmente les coûts et crée un nouveau mode de panne sans ajouter de confiance supplémentaire.

La règle d’or : si le bug peut être détecté sans navigateur, détectez-le sans navigateur.

Bonnes pratiques

  • Écrivez peu de tests e2e et faites en sorte que chacun représente un parcours critique.
  • Initialisez vos données via l’API et lancez les tests en étant déjà authentifié.
  • Utilisez des locateurs basés sur le rôle, le label et le texte ; ne traitez les test ids qu’en dernier recours.
  • Attendez que les conditions soient remplies avec des assertions “web-first”, jamais avec waitForTimeout.
  • Isolez les données par test ou par worker pour que l’ordre d’exécution n’ait aucune importance.
  • Limitez-vous à un seul parcours par test afin qu’un échec identifie clairement la cause.
  • Pointez votre suite de tests vers BASE_URL ; exécutez-la localement, en CI et sur vos environnements de preview.
  • Capturez les traces et les vidéos en cas d’échec et téléchargez-les en tant qu’artefacts CI.
  • Mettez en quarantaine et corrigez les tests instables (flakes) ; ne considérez jamais les tentatives de relance (retries) comme une solution.
  • Conservez au moins un test de connexion réel, même lorsque vous réutilisez storageState.

Erreurs courantes

  • Se connecter via l’UI avant chaque test.
  • Sélectionner des éléments par classe CSS ou par position dans le DOM.
  • Ajouter waitForTimeout pour « corriger » une race condition.
  • Partager un utilisateur ou un enregistrement seedé entre plusieurs tests.
  • Exécuter les tests sur un serveur local lancé manuellement plutôt que sur une stack reproductible.
  • Tester la logique de niveau unitaire via le navigateur.
  • Hard-coder localhost:3000, rendant la suite de tests exécutable sur une seule machine.
  • Ne télécharger aucun artefact, puis se retrouver incapable de déboguer un échec en CI.
  • Laisser un test instable (flaky) en état d’échec et de relance automatique au lieu de le mettre en quarantaine.

Et après ?

Les tests E2E se situent au sommet de la pyramide, et ils sont d’autant plus efficaces que les couches inférieures sont saines. Consultez Supertest pour mettre en place des tests API rapides couvrant la majeure partie de votre surface HTTP, et Playwright pour explorer l’outil plus en profondeur. Si vous envisagez d’autres alternatives, Cypress présente l’autre runner majeur, et Docker explique comment déployer la stack reproductible dont dépend votre suite de tests.

En pratique

Quatre éléments d'une suite e2e réelle

Un parcours, un raccourci de connexion, des données initialisées et le job CI qui les exécute.

tests/checkout.spec.ts
import { test, expect } from "@playwright/test";

test("a signed-in user can upgrade to Pro", async ({ page }) => {
  await page.goto("/pricing");

  await page.getByRole("button", { name: "Choose Pro" }).click();
  await expect(page).toHaveURL(/\/checkout/);

  await page.getByLabel("Card number").fill("4242 4242 4242 4242");
  await page.getByLabel("Expiry").fill("12/30");
  await page.getByLabel("CVC").fill("123");
  await page.getByRole("button", { name: "Pay" }).click();

  await expect(
    page.getByRole("heading", { name: "You're on Pro" }),
  ).toBeVisible();
});

Locateurs basés sur les rôles vs sélecteurs CSS

Les locateurs de rôles et de labels décrivent ce que l'utilisateur voit et échouent lorsque l'élément est inaccessible. Les sélecteurs CSS cassent à chaque refactorisation et testent silencieusement le markup plutôt que le comportement.

Préférer
await page
  .getByRole("button", { name: "Add to cart" })
  .click();
Éviter
await page
  .locator("div.product > button.btn.btn--primary")
  .click();

Attendre une condition vs sleep

Les assertions web-first réessaient jusqu'à ce que la condition soit remplie. Un sleep fixe est soit trop court et instable, soit trop long et lent, et il masque la race condition qu'il était censé pallier.

Préférer
await expect(
  page.getByText("Order confirmed"),
).toBeVisible();
Éviter
await page.waitForTimeout(5000);
await expect(
  page.getByText("Order confirmed"),
).toBeVisible();

Compromis

Le coût réel des tests de bout en bout

Les tests E2E sont les seuls à prouver qu'un utilisateur peut terminer une tâche, et les plus coûteux à maintenir. Utilisez-les avec parcimonie.

Strengths

  • Ils testent ce que font les utilisateurs

    Seul un test e2e sollicite l'UI, l'API et la base de données réelles ensemble, capturant ainsi les bugs de câblage qu'aucun test unitaire ne peut voir.

  • Ils survivent aux refactorisations

    Écrit contre des rôles et du texte visible, un test de parcours continue de passer alors que les composants, frameworks et API internes changent.

  • Ils constituent un langage commun

    Le produit, la QA et l'ingénierie peuvent lire la même spécification et s'accorder sur ce que signifie « fonctionner ».

Trade-offs

  • Ils sont lents et coûteux

    Un test navigateur prend plusieurs secondes et nécessite une infrastructure réelle ; une suite de centaines de tests transforme chaque pull request en pause café.

  • Ils sont instables par nature

    Le timing, les réseaux, les animations et les données partagées conspirent pour faire échouer les tests qui ne sont pas soigneusement isolés et attendus.

  • Ils donnent des échecs vagues

    Un test de parcours rouge vous indique que quelque chose a cassé dans un long chemin. Identifier quoi demande des traces et, souvent, une reproduction locale.

FAQ

Foire aux questions

Keep learning

Related topics from the roadmap.

$ commencer à apprendre

Prêt à apprendre End-to-End Testing ?

Notre tutoriel interactif vous guide à travers End-to-End Testing pas à pas — avec des quiz et du vrai code que vous pouvez exécuter dans le navigateur.