Vous pushez une petite modification, la pipeline s'exécute et un test échoue. Vous le relancez sans aucun changement de code, et il passe. Rien n'a changé sur votre application entre ces deux exécutions. C'est un test "flaky" (instable), et quiconque a passé un temps sérieux sur Playwright connaît ce nœud à l'estomac lorsque cela se produit.

Ce que personne ne dit aux ingénieurs QA juniors : la flakiness n'a généralement rien à voir avec le fait que l'application soit cassée. Cela signifie qu'un test a fait une supposition sur le timing qui n'est vraie que de temps en temps.

C'est rare que ce soit l'application

Lorsqu'un test échoue de manière sporadique, le premier réflexe est d'incriminer la fonctionnalité. Dans neuf cas sur dix, le véritable problème est que le test clique sur un bouton avant qu'il ne soit réellement cliquable, ou vérifie un texte avant que l'appel API qui le génère ne soit terminé. L'application fonctionnait parfaitement. Le test n'a tout simplement pas attendu qu'elle ait fini.

Cette distinction est importante car elle change ce que vous réparez. Traquer un bug fantôme dans le code de l'application alors que rien n'est cassé fait perdre un après-midi entier pour rien.

Les suspects habituels

Quelques schémas reviennent sans cesse dès que l'on remonte aux causes profondes des tests flaky :

  • Race Conditions (situations de compétition) : Le test interagit avec un élément avant que la page n'ait fini de le rendre ou avant qu'une requête réseau ne soit complétée. C'est le plus gros morceau, facilement 70 % de ce que je rencontre.
  • Sélecteurs dépendants du style : Un test qui cible .btn-primary-v2 casse à l'instant même où un développeur renomme une classe. La fonctionnalité n'a pas changé, mais le test si.
  • État partagé entre les tests : Le test B suppose que la base de données est dans l'état où le test A l'a laissée. Si vous les exécutez dans un autre ordre ou en parallèle, le test B échoue pour des raisons qui n'ont rien à voir avec lui.
  • Bruit environnemental : Des runners CI plus lents, des caches froids, des API tierces qui passent une mauvaise journée. Cela existe vraiment, mais c'est aussi l'excuse à laquelle on se raccroche lorsque la cause réelle est l'une des trois mentionnées ci-dessus.

Réparez d'abord les attentes (waits)

Playwright a déjà résolu la majeure partie du problème de timing pour vous, et beaucoup de suites de tests flaky n'utilisent justement pas cette partie. L'erreur classique :

await page.click('.submit-button');
await page.waitForTimeout(3000);
expect(await page.textContent('.success-message')).toBe('Order confirmed');

Ce waitForTimeout(3000) est une devinette. Parfois, 3 secondes suffisent amplement, parfois le serveur est lent ce jour-là et cela ne suffit pas, et de toute façon, soit vous perdez du temps, soit vous continuez à vous heurter à la page. Les locators de Playwright attendent automatiquement pour vous, donc ceci fait exactement le même travail, mais correctement :

await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Order confirmed')).toBeVisible();

expect(locator).toBeVisible() interroge la page en boucle jusqu'à ce que l'élément apparaisse ou qu'un timeout soit atteint, sans pause arbitraire et sans devinette. J'estimerais que la moitié des tests flaky que j'ai débogués l'année dernière consistaient simplement à remplacer un waitForTimeout par une véritable assertion comme celle-ci.

Isolez vos données de test

Si votre test de connexion et votre test de paiement accèdent tous deux à test@example.com, vous avez créé une dépendance entre deux tests qui ne devraient même pas se connaître. Les restes de panier de l'un ruinent discrètement l'autre.

La solution n'a rien de spectaculaire : générer des données de test uniques par exécution. Un horodatage dans l'adresse e-mail, un nouvel utilisateur dans le beforeEach, peu importe ce qui s'intègre à votre configuration. Cela représente plus de code au début. Mais cela signifie aussi que vous pouvez exécuter votre suite en parallèle sans que les tests ne se marchent sur les pieds, ce qui compte plus qu'on ne le croit dans un budget CI.

Les retries sont un pansement, pas une guérison

Playwright vous permet de configurer des retries (tentatives de réexécution) dans la CI, et je les utilise, mais je veux être honnête sur ce qu'ils font réellement : ils cachent le problème, ils ne le résolvent pas. Un test qui échoue une fois et passe au retry reste un test flaky. Il a juste échoué de manière trop discrète pour que quelqu'un le remarque.

Je laisse tout de même les retries activés dans la CI, car une pipeline rouge qui bloque un déploiement à cause d'un simple hoquet de timing est une autre forme de gaspillage. Cependant, je surveille le nombre de fois où chaque test a besoin d'un retry pour passer. Un test qui a constamment besoin de retries est signalé et réécrit, il n'est pas laissé de côté sous prétexte qu'il finit par "devenir vert".

Ce que cela vous apporte au final

Rien de tout cela n'est compliqué une fois qu'on l'a vu, mais j'ai mis plus de temps que je ne voudrais l'admettre à arrêter de traiter les tests flaky comme un simple bruit de fond. Une suite de tests en laquelle personne n'a confiance est pire que pas de suite du tout, car sans tests, au moins, personne n'a une fausse sécurité. Au moment où quelqu'un dans l'équipe dit "ah, ce test échoue parfois, ignore-le", vous avez perdu ce que les tests sont censés vous apporter.

Réparez les attentes, isolez les données et traitez les retries comme un signal plutôt que comme une solution. Ce travail n'est pas glamour, mais il fait toute la différence entre une suite de tests en laquelle vous avez confiance et une autre avec laquelle vous composez tant bien que mal.