Sleepy Test.
Un Sleepy Test met l'exécution en pause avec un délai codé en dur (`Thread.sleep`, `setTimeout`, `cy.wait(2000)`, `page.waitForTimeout`) pour attendre un travail asynchrone, au lieu d'attendre la condition réelle.
##Signs and Symptoms
Vous voyez un délai à nombre magique trôner entre une action et son assertion, avec un commentaire qui s'en excuse :
test('order is processed', async () => {
submitOrder(order);
// laisser au worker le temps de finir
await new Promise((r) => setTimeout(r, 2000));
expect(await getOrderStatus(order.id)).toBe('processed');
});
Signes révélateurs :
- Des sleeps fixes dans les corps de tests ou les hooks :
Thread.sleep(500),await sleep(1000),time.sleep(2),cy.wait(3000),await page.waitForTimeout(2000),browser.pause(2000). - Des commentaires comme
// wait for the animation,// let the DB catch up,// flaky without this. - La durée du sleep dérive vers le haut au fil du temps (
1000devient2000devient5000) à mesure que les gens combattent les échecs intermittents en rembourrant le délai. - Le nombre est la seule synchronisation — il n'y a aucune assertion, scrutation, ou événement auquel l'attente est réellement liée.
- Une suite lente où le temps d'horloge est dominé par les sleeps plutôt que par le travail réel.
La mauvaise odeur concerne l'attente inconditionnelle. cy.wait('@apiAlias') ou await expect(locator).toBeVisible() attendent une condition et sont acceptables ; cy.wait(2000) et page.waitForTimeout(2000) attendent l'horloge et ne le sont pas.
##Reasons for the Problem
Pourquoi cela se produit
- Une opération asynchrone (une file, un minuteur, une animation, un appel réseau, un thread d'arrière-plan) n'offre aucun point d'ancrage évident où attendre, alors un délai est la voie de moindre résistance.
- Un test échouait par intermittence et quelqu'un l'a « corrigé » en insérant ou en allongeant un sleep jusqu'à ce qu'il passe au vert sur sa machine.
- Le test interagit avec un système externe réel (horloge, ordonnanceur, système de fichiers, service HTTP) dont l'achèvement n'est pas observable, alors on devine le timing.
Pourquoi c'est nuisible
- Fiabilité / fausse confiance. Un sleep encode une hypothèse — « le travail se termine en N ms ». Le temps de traitement varie selon les machines, la charge de la CI et les exécutions, donc le test est non déterministe : il passe en local et échoue sur un agent de CI chargé, ou pire, le sleep est trop court et l'assertion fait la course avec le code, ne passant que par chance de timing. C'est l'une des causes profondes les plus citées des tests instables.
- Lenteur. Un sleep fixe attend toujours toute la durée, même quand le travail s'est terminé en 20 ms. Multipliés sur toute une suite, les sleeps transforment des secondes de travail réel en minutes d'attente oisive, ce qui décourage d'exécuter les tests souvent.
- Maintenabilité. Le nombre magique est fragile : accélérer ou ralentir le système testé casse silencieusement le contrat de timing, et la seule « correction » à laquelle on pense est d'augmenter le nombre — un cliquet qui rend la suite plus lente et toujours instable.
- Lisibilité. Le délai cache ce que le test attend réellement. Un lecteur ne peut pas dire si
sleep(2000)protège une écriture en base, un rendu, ou rien du tout, de sorte que l'intention du test est obscurcie.
##Treatment
Remplacez « attendre une durée fixe » par « attendre la condition ». Identifiez le signal observable indiquant que le travail asynchrone est terminé, puis bloquez sur celui-ci avec un délai d'attente généreux.
- Scrutez la condition. Utilisez un assistant de scrutation/réessai — Awaitility (JVM),
vi.waitFor/waitFor(Vitest, Testing Library), lewaitForde Jest, ou les assertions auto-réessayantes de votre exécuteur de tests — pour que le test progresse à l'instant où la condition devient vraie et n'échoue qu'après un délai d'attente. - Préférez les assertions attendues intégrées. Les outils E2E web réessaient déjà : les assertions « web-first » de Playwright (
await expect(locator).toBeVisible()) et les commandes auto-réessayantes de Cypress suppriment tout besoin d'attendre. - Attendez des événements, pas l'horloge. Attendez la promesse,
awaitla réponse réseau, ou utilisez unCountDownLatch/callback/waitFor('@alias')que le code de production signale réellement. - Contrôlez le temps au lieu de le dépenser. Lorsque le délai est un véritable minuteur dans le code testé, utilisez des faux minuteurs (
jest.useFakeTimers(),vi.useFakeTimers(),vi.advanceTimersByTimeAsync) pour faire avancer l'horloge de manière déterministe plutôt que de dormir.
Avant :
test('order is processed', async () => {
submitOrder(order);
await new Promise((r) => setTimeout(r, 2000)); // sleepy
expect(await getOrderStatus(order.id)).toBe('processed');
});
Après (scruter la condition avec un délai d'attente borné) :
import { vi } from 'vitest';
test('order is processed', async () => {
submitOrder(order);
await vi.waitFor(
async () => expect(await getOrderStatus(order.id)).toBe('processed'),
{ timeout: 5000, interval: 50 },
);
});
Équivalents Playwright/Cypress :
// Playwright — l'assertion web-first réessaie automatiquement jusqu'à la visibilité ou le timeout
await expect(page.getByText('processed')).toBeVisible();
// Cypress — attendre la requête, pas un nombre
cy.intercept('POST', '/orders').as('createOrder');
cy.wait('@createOrder');
Le résultat n'attend pas plus longtemps que nécessaire, échoue vite avec un message de délai d'attente clair, et ne dépend plus de la vitesse de la machine.
##Detected by
- sonar java:S2925 — « Thread.sleep » ne devrait pas être utilisé dans les tests
- eslint-plugin-playwright playwright/no-wait-for-timeout — no-wait-for-timeout (interdit page.waitForTimeout)
- eslint-plugin-cypress cypress/no-unnecessary-waiting — no-unnecessary-waiting (interdit cy.wait avec un nombre)
- eslint-plugin-ui-testing ui-testing/no-hard-wait — no-hard-wait (cy.wait, page.waitForTimeout, browser.pause, t.wait)