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
Inscription
Formulaire /register, dans un navigateur vierge
2
Assistant en 6 étapes
Intention, taille, secteur NAF, commune, signaux, récap
3
Veille du jour
La tâche planifiée matérialise les événements BODACC
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).

Les instructions que je donne à l'IA sur ce projet le désignent comme le test à lancer en premier, avant toute autre vérification :
npx playwright test parcours-nouvel-utilisateurLe 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 :
// 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.
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ôme | Cause habituelle | Piste |
|---|---|---|
| Erreur 429 à la connexion | Limite de tentatives atteinte | Réutiliser une session enregistrée |
| Jeton CSRF manquant | Le jeton n'est pas encore propagé | Naviguer sur l'application avant d'appeler l'API |
| Données d'un autre test | Pas d'isolation entre scénarios | Remettre à zéro avant chaque scénario, ou des données uniques |
| Résultat périmé | Un cache survit à la remise à zéro | Purger ce cache ou changer sa clé |
| Sélecteur introuvable | Un texte ou une classe CSS a changé | Chercher par rôle, libellé ou placeholder |
| Passe seul, échoue en suite | Les tests dépendent de leur ordre | Rendre 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 :
- Le parcours qui fait la valeur du produit, toujours.
- Un ou deux cas limites prioritaires : liste vide, erreur d'un service, droits insuffisants.
- 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.
