• tests
  • e2e

Tests end-to-end : le test qui prouve que le produit tient debout

Un vrai navigateur, un vrai parcours, de l'inscription au premier résultat : mes tests Playwright sur Le Limier, et le bug qu'un rechargement masquait.

6 min de lecture

localhost:8090/register1Inscription2Radar3VeilleProspect

Les tests unitaires vérifient les pièces une par une. Un test end-to-end (E2E) ouvre un vrai navigateur et fait ce que fait un client : remplir un formulaire, cliquer, attendre, vérifier ce qui s'affiche. C'est lent, parfois capricieux, et c'est pourtant le seul test qui voit l'application comme la voit un utilisateur. Sur Le Limier, 38 scénarios Playwright tournent ainsi, et l'un d'eux compte plus que tous les autres.

Ce qu'un test E2E voit, et pas les autres

Dans l'article sur les tests, je raconte comment une suite entièrement verte cohabitait avec une application cassée : chaque formulaire envoyé après la connexion était refusé avec une erreur 419. Ce code signifie que le jeton de sécurité CSRF (qui protège les formulaires contre les envois frauduleux) n'est plus valide. Les tests unitaires ne pouvaient pas le voir : le problème n'existait que dans un navigateur, avec une vraie session, après une vraie connexion.

38

scénarios Playwright

12

fichiers, un par parcours ou écran

1

parcours « bus factor » à lancer en premier

Le test « bus factor »

Le « bus factor », c'est la question : si le développeur disparaît demain, quelqu'un peut-il reprendre ? Ce test y répond pour la partie qui compte le plus. Son en-tête le dit ainsi :

C'est le test « bus factor » : s'il passe, quelqu'un qui reprend le projet
sait que la chaîne qui fait la valeur du produit tient debout.

Il part d'un navigateur vierge, crée un compte par le formulaire d'inscription, traverse l'assistant de création de radar, déclenche la veille du jour et enregistre un prospect. À la fin, il vérifie par l'API que le prospect existe bien, et qu'il appartient au nouvel inscrit.

  1. 1

    Inscription

    Formulaire /register, dans un navigateur vierge

  2. 2

    Assistant en 6 étapes

    Intention, taille, secteur NAF, commune, signaux, récap

  3. 3

    Veille du jour

    La tâche planifiée matérialise les événements BODACC

  4. 4

    Prospect enregistré

    Un clic, puis vérification par l'API : 1 prospect, au bon compte

Pour de vraiSession, jeton CSRF, nomenclature NAF, création du radar, veille BODACC, enregistrement.

Simulé dans le navigateurL'API d'adresses (service externe) et les suggestions IA (payantes, non déterministes).

Le parcours « bus factor » de Le Limier : un seul test, toute la chaîne qui fait la valeur du produit.
L'assistant de création d'un radar de Le Limier, que le test traverse étape par étape
L'assistant en six étapes, parcouru par le test exactement comme par un utilisateur (compte de démonstration).

Les instructions que je donne à l'IA sur ce projet le désignent comme le test à lancer en premier, avant toute autre vérification :

bash
npx playwright test parcours-nouvel-utilisateur

Le détail qui a tout changé : ne jamais recharger la page

Les premiers tests E2E naviguaient avec page.goto() : un chargement complet de la page à chaque étape. Pratique, et piégeux. Chaque rechargement récupérait un jeton CSRF tout neuf, ce qui masquait exactement le bug 419 : un vrai utilisateur, lui, ne recharge pas, il clique.

Le parcours actuel s'interdit donc tout rechargement après l'inscription, et le commentaire en garde la raison :

ts
// Aucun `page.goto` d'ici la fin : tout le parcours reste en navigation
// Inertia, sans rechargement, comme pour un vrai utilisateur. Un
// rechargement masquait le 419 qui frappait chaque appel après la
// connexion (jeton CSRF figé dans la balise <meta>, corrigé le 2026-10-11).
await expect(page).toHaveURL(/\/radars$/, { timeout: 10_000 })

Pour revenir au radar, le test passe par le menu puis par la carte du radar, comme une personne le ferait.

Simuler uniquement ce qui sort du périmètre

Deux appels seulement sont interceptés dans le navigateur, pour des raisons précises : l'API publique d'adresses, un service externe, et les suggestions IA de l'assistant, un appel payant dont la réponse change à chaque fois.

ts
async function stubExternalCalls(page: Page): Promise<void> {
    await page.route('https://api-adresse.data.gouv.fr/**', (route) =>
        route.fulfill({ json: { features: [/* la commune des fixtures */] } }),
    )
 
    await page.route('**/api/v1/radars/suggest-from-intent', (route) =>
        route.fulfill({ json: { success: true, data: { suggested_sectors: [] /* … */ } } }),
    )
}

Couper l'IA ne casse pas le parcours : l'assistant est conçu pour fonctionner sans ses suggestions. Le test le prouve au passage. Tout le reste tourne pour de vrai : session, jeton CSRF, nomenclature des secteurs d'activité, création du radar, veille sur les annonces légales, enregistrement.

Des données de test qu'on ne peut pas confondre avec des vraies

Les données utilisées par les tests sont volontairement impossibles dans la réalité : une commune au code 99999, des numéros SIREN qui commencent par 9999999, des comptes inscrits sur un domaine en .test, purgés avant et après chaque passage. Impossible de les confondre avec une vraie entreprise, et impossible de polluer une vraie donnée.

Ces données sont créées par l'application elle-même, via des commandes dédiées (lelimier:e2e:seed-fixtures), jamais par des requêtes SQL écrites dans les tests. La raison est notée dans le code : la base est manipulée uniquement par PHP, avec les mêmes règles qu'en production. Sinon, les tests vérifieraient des données que l'application n'aurait jamais pu produire.

Une connexion, pas cinquante

Le formulaire de connexion de Le Limier est limité à cinq tentatives par minute, une protection contre les attaques par force brute. Une suite de 38 tests qui se connecte à chaque test se bloque toute seule.

La tentation est de désactiver la limite pendant les tests. C'est interdit dans ma méthode : on testerait alors une application qui n'existe pas en production. À la place, une préparation unique se connecte une fois par l'interface, enregistre la session, et tous les tests la réutilisent. Seul le parcours « bus factor » part d'un navigateur vierge, parce que l'inscription et la connexion font précisément partie de ce qu'il vérifie.

Les tests instables ont des causes connues

Un test E2E qui passe une fois sur deux ne doit jamais être « relancé jusqu'à ce que ça passe ». Ma skill de tests E2E tient la liste des causes que j'ai déjà rencontrées :

SymptômeCause habituellePiste
Erreur 429 à la connexionLimite de tentatives atteinteRéutiliser une session enregistrée
Jeton CSRF manquantLe jeton n'est pas encore propagéNaviguer sur l'application avant d'appeler l'API
Données d'un autre testPas d'isolation entre scénariosRemettre à zéro avant chaque scénario, ou des données uniques
Résultat périméUn cache survit à la remise à zéroPurger ce cache ou changer sa clé
Sélecteur introuvableUn texte ou une classe CSS a changéChercher par rôle, libellé ou placeholder
Passe seul, échoue en suiteLes tests dépendent de leur ordreRendre chaque scénario autonome

Les tests de Le Limier tournent d'ailleurs un par un, sur une seule instance : ils touchent une vraie base de données, et deux tests en parallèle se marcheraient dessus.

Ce que je ne teste pas en E2E

Ces tests coûtent cher : ils sont lents et demandent de l'entretien. Je les réserve à ce que les autres niveaux ne peuvent pas voir, dans cet ordre :

  1. Le parcours qui fait la valeur du produit, toujours.
  2. Un ou deux cas limites prioritaires : liste vide, erreur d'un service, droits insuffisants.
  3. Un test par bug grave déjà arrivé en production, pour qu'il ne revienne pas.

La validation détaillée d'un formulaire ou toutes les combinaisons d'un menu déroulant relèvent des tests unitaires, bien plus rapides.

Ce que ça vous garantit

Quand je livre une évolution sur une application que je maintiens, le parcours principal de vos utilisateurs a été rejoué dans un vrai navigateur, du premier clic au résultat. C'est la différence entre « le code est correct » et « vos clients peuvent s'en servir ». C'est aussi ce qui permet de faire travailler des agents IA sur le code sans craindre qu'ils cassent l'essentiel en silence. Le reste de la démarche est sur la page méthode.

Portrait illustré d'Antoine Lemariey

Antoine Lemariey

Développeur et fondateur de Not a Bad Company. Je conçois et je code moi-même chaque projet, de la spécification à la mise en ligne : PostOwl, Le Limier, BookFlow, AntiScam Detector…

À lire ensuite

Un projet à construire avec cette rigueur ?

Décrivez-moi votre besoin, je vous dis comment je le construirais.

Parlons de votre projet