Tests de bout en bout : tester l'application comme un utilisateur
L'idée en une phrase
Un test de bout en bout (end-to-end, ou E2E) vérifie l'application entière, comme un utilisateur : un vrai navigateur ouvre la page, remplit le formulaire, clique sur le bouton, et vérifie ce qui s'affiche. Le front-end, le back-end, la base de données et le réseau fonctionnent ensemble, pour de vrai. C'est le test le plus réaliste, mais aussi le plus lent et le plus fragile : on en écrit peu, sur les parcours essentiels.
Une image pour comprendre : l'essai sur route
Dans l'usine des fiches précédentes, chaque pièce a passé son banc d'essai (tests unitaires), puis les assemblages leur banc de montage (tests d'intégration). Il reste une dernière étape avant de livrer : l'essai sur route.
Un pilote prend la voiture terminée, démarre, accélère, freine, se gare. Il ne vérifie pas chaque pièce : il vérifie que la voiture fait ce qu'un conducteur attend.
- Très réaliste : si l'essai passe, la voiture roule vraiment.
- Long et coûteux : on ne fait pas un essai sur route pour chaque vis.
- Peu précis : si la voiture cale, il faut encore chercher pourquoi.
C'est exactement le rôle, et les limites, d'un test E2E.
La place des tests E2E
| Type de test | Ce qui est vérifié | Vitesse | Nombre |
|---|---|---|---|
| unitaire | une classe seule | millisecondes | des centaines |
| intégration | plusieurs couches ensemble | secondes | des dizaines |
| bout en bout | l'application entière, par l'interface | secondes à minutes | quelques-uns |
On parle souvent de pyramide des tests : beaucoup de tests unitaires à la base, moins de tests d'intégration au milieu, et quelques tests E2E au sommet.
L'erreur classique est de tout tester en E2E « parce que c'est le plus réaliste ». On obtient des centaines de tests lents, qui échouent au hasard, et que plus personne n'ose toucher. Une règle métier se teste en unitaire ; un parcours utilisateur complet, en E2E.
Que tester en E2E ?
Les parcours critiques : ceux qui, s'ils cassent, empêchent les utilisateurs de se servir de l'application.
| ✅ À tester en E2E | ❌ À tester ailleurs |
|---|---|
| se connecter | chaque règle de validation d'un champ (unitaire, @WebMvcTest) |
| créer un compte | le calcul exact d'un total avec remise (unitaire) |
| passer une commande, de la recherche au paiement | chaque cas d'erreur d'une API (intégration) |
| le parcours principal de chaque grand écran | les détails d'affichage (tests du front-end) |
Pose-toi la question : « Si ce parcours cassait en production, le saurait-on par un appel d'un utilisateur mécontent ? » Si oui, il mérite un test E2E.
Les outils
| Outil | Points forts |
|---|---|
| Playwright | récent, rapide, attend automatiquement que les éléments soient prêts ; s'écrit en Java, TypeScript, Python… |
| Cypress | très répandu côté front-end, écrit en JavaScript ou TypeScript |
| Selenium | le plus ancien, encore présent dans beaucoup d'entreprises |
Les exemples de cette fiche utilisent Playwright en Java. Dans beaucoup d'équipes, les tests E2E sont écrits en TypeScript par l'équipe front-end : la syntaxe change, mais les principes sont les mêmes.
Un premier test avec Playwright
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>...</version>
<scope>test</scope>
</dependency>
Au premier lancement, Playwright télécharge les navigateurs dont il a besoin.
class ConnexionE2ETest {
static Playwright playwright;
static Browser navigateur;
Page page;
@BeforeAll
static void demarrerNavigateur() {
playwright = Playwright.create();
navigateur = playwright.chromium().launch(); // sans fenêtre visible par défaut
}
@BeforeEach
void ouvrirPage() {
page = navigateur.newContext().newPage(); // un contexte neuf : aucun cookie, aucune session
}
@Test
void un_utilisateur_peut_se_connecter() {
page.navigate(URL_APPLICATION + "/connexion");
page.getByLabel("Adresse e-mail").fill("alice@exemple.fr");
page.getByLabel("Mot de passe").fill(MOT_DE_PASSE_TEST);
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Se connecter")).click();
assertThat(page.getByText("Bonjour Alice")).isVisible();
}
@AfterAll
static void fermerNavigateur() {
playwright.close();
}
}
Le test se lit presque comme la description d'un utilisateur : ouvrir la page de connexion, remplir l'e-mail, remplir le mot de passe, cliquer sur « Se connecter », voir « Bonjour Alice ».
Note :
assertThatvient ici de Playwright (PlaywrightAssertions), pas d'AssertJ. Il attend que la condition soit vraie (jusqu'à quelques secondes) avant d'échouer. C'est indispensable : la page met toujours un peu de temps à afficher le résultat.
Trouver les éléments de la page
C'est la clé de tests E2E stables. Un test qui cherche les éléments de façon fragile casse à chaque modification de la page, même quand l'application fonctionne parfaitement.
| Façon de trouver l'élément | Exemple | Solidité |
|---|---|---|
| par son rôle et son texte | getByRole(AriaRole.BUTTON, ...setName("Se connecter")) |
✅ très solide : c'est ce que voit l'utilisateur |
| par son libellé | getByLabel("Adresse e-mail") |
✅ très solide |
| par un identifiant de test | getByTestId("bouton-valider-commande") |
✅ solide, prévu pour les tests |
| par une classe CSS | locator(".btn-primary") |
❌ casse dès qu'on change le style |
| par la structure de la page | locator("div > div:nth-child(3) > button") |
❌ casse dès qu'on déplace un élément |
Chercher les éléments comme un utilisateur les voit (par leur rôle, leur libellé, leur texte) a un double avantage : le test casse moins, et il vérifie au passage que la page est accessible (un champ sans libellé est introuvable par getByLabel… et par un lecteur d'écran).
Quand aucun texte ne convient, l'équipe front-end peut ajouter un attribut data-testid="..." sur l'élément, qui sert uniquement aux tests.
Les tests instables : l'ennemi n°1
Un test E2E qui échoue « une fois sur dix » sans raison apparente est un test instable (flaky). C'est le problème principal des tests E2E : à force de relancer le pipeline en espérant qu'il passe, plus personne ne fait confiance aux tests, et les vrais bugs passent inaperçus.
Les causes les plus fréquentes, et leur remède :
Attendre avec Thread.sleep
page.getByRole(AriaRole.BUTTON, ...).click();
Thread.sleep(2000); // ❌ trop court les jours de lenteur, du temps perdu les autres
assertThat(page.getByText("Commande validée")).isVisible();
Playwright attend automatiquement que les éléments soient visibles et cliquables, et ses vérifications patientent jusqu'à ce que la condition soit vraie. N'écris jamais de Thread.sleep dans un test E2E. Si un test semble avoir besoin d'une pause, il attend en réalité quelque chose de précis : vérifie cette chose (un texte qui apparaît, un chargement qui disparaît).
Des données partagées entre les tests
Deux tests qui utilisent le même compte, le même produit ou le même panier finissent par se gêner, surtout s'ils tournent en parallèle. Chaque test crée ses propres données :
- un utilisateur ou une commande dédiés au test, avec des valeurs uniques (
alice-suivi d'un identifiant aléatoire) ; - de préférence créés par l'API, et non par l'interface : c'est beaucoup plus rapide, et le test ne vérifie que le parcours qui l'intéresse.
Dépendre de l'ordre des tests
Un test qui ne passe que si un autre s'est exécuté avant (« le compte a été créé par le test précédent ») est fragile par construction. Chaque test doit pouvoir s'exécuter seul, dans n'importe quel ordre.
Quand un test E2E échoue
Un test E2E qui échoue dit qu'un parcours est cassé, mais rarement pourquoi : le problème peut venir du front-end, du back-end, de la base, du réseau ou du test lui-même. Il faut des indices.
Playwright peut enregistrer une trace : une capture de chaque étape, avec les requêtes réseau, les messages de la console et l'état de la page à chaque action.
BrowserContext contexte = navigateur.newContext();
contexte.tracing().start(new Tracing.StartOptions().setScreenshots(true).setSnapshots(true));
// ... le test ...
contexte.tracing().stop(new Tracing.StopOptions().setPath(Paths.get("target/traces/connexion.zip")));
Le fichier s'ouvre dans le visualiseur de traces de Playwright (le site trace.playwright.dev, sans rien installer). On y rejoue le test pas à pas, et on voit exactement ce que le navigateur affichait au moment de l'échec.
Dans le pipeline, conserve ces traces (et des captures d'écran) comme artefacts du job, au moins quand un test échoue (voir la fiche sur les pipelines CI/CD).
La méthode pour analyser un échec :
- ouvre la trace ou la capture : qu'affichait la page au moment de l'échec ?
- regarde les requêtes réseau : une API a-t-elle renvoyé une erreur 500 ? Si oui, le problème est dans le back-end : cherche dans ses logs ;
- relance le test une fois : s'il passe, c'est peut-être un test instable. Ne t'arrête pas là : signale-le, et cherche la cause.
Où s'exécutent les tests E2E ?
Les tests E2E ont besoin de l'application complète en fonctionnement : front-end, back-end, base de données.
| Où | Comment |
|---|---|
| sur ton poste | l'application démarrée en local, souvent avec Docker Compose pour la base et les autres services |
| dans le pipeline, sur l'environnement de recette | les tests s'exécutent juste après le déploiement automatique en recette (voir la fiche sur les pipelines) |
| en production | seulement quelques tests très simples, en lecture seule : la page d'accueil s'affiche, la connexion fonctionne. On les appelle des smoke tests |
L'adresse de l'application ne doit pas être écrite en dur : elle vient de la configuration, pour que les mêmes tests tournent partout.
static final String URL_APPLICATION =
System.getenv().getOrDefault("URL_APPLICATION", "http://localhost:8080");
Note : les comptes de test et leurs mots de passe ne sont jamais écrits dans le code. Comme tous les secrets, ils viennent des variables du pipeline (voir la fiche sur les pipelines CI/CD). Et un test E2E qui crée, modifie ou supprime des données ne s'exécute jamais en production.
Garder les tests lisibles : les Page Objects
Quand plusieurs tests utilisent la même page, répéter getByLabel("Adresse e-mail") partout devient pénible : si le libellé change, il faut modifier tous les tests. La solution habituelle est de regrouper la description d'une page dans une classe, un Page Object :
public class PageConnexion {
private final Page page;
public PageConnexion(Page page) {
this.page = page;
}
public void ouvrir() {
page.navigate(URL_APPLICATION + "/connexion");
}
public void seConnecter(String email, String motDePasse) {
page.getByLabel("Adresse e-mail").fill(email);
page.getByLabel("Mot de passe").fill(motDePasse);
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Se connecter")).click();
}
}
@Test
void un_utilisateur_peut_se_connecter() {
PageConnexion connexion = new PageConnexion(page);
connexion.ouvrir();
connexion.seConnecter("alice@exemple.fr", MOT_DE_PASSE_TEST);
assertThat(page.getByText("Bonjour Alice")).isVisible();
}
Le test ne décrit plus comment on se connecte, mais ce que fait l'utilisateur. Et si la page de connexion change, une seule classe est à modifier.
Les bonnes habitudes
- Peu de tests E2E, sur les parcours critiques : connexion, création de compte, commande. Le reste se teste en unitaire et en intégration.
- Trouve les éléments comme un utilisateur : par rôle, libellé ou texte, ou par
data-testid. Jamais par classe CSS ni par structure. - Jamais de
Thread.sleep: utilise les attentes automatiques de l'outil. - Chaque test crée ses propres données, de préférence par l'API, avec des valeurs uniques.
- Chaque test s'exécute seul, dans n'importe quel ordre.
- Traces et captures d'écran conservées quand un test échoue.
- Un test instable est un bug à corriger, pas un test à relancer jusqu'à ce qu'il passe.
- L'adresse et les comptes de test viennent de la configuration, jamais du code.
- En production, seulement des smoke tests en lecture seule.
- Des Page Objects dès que plusieurs tests utilisent la même page.
À retenir
- Un test E2E vérifie l'application entière, par l'interface, comme un utilisateur.
- Il est le plus réaliste, mais aussi le plus lent et le plus fragile : on en écrit peu, sur les parcours critiques.
- Playwright pilote un vrai navigateur et attend automatiquement que la page soit prête.
- La stabilité dépend surtout de la façon de trouver les éléments, de l'absence de
Thread.sleepet de données propres à chaque test. - Un échec E2E dit qu'un parcours est cassé ; la trace aide à trouver pourquoi.
- Les tests E2E tournent sur un environnement complet, en général la recette, juste après le déploiement.