Sleepy Test.
Una Sleepy Test pausa la ejecución con un retardo codificado de forma fija (`Thread.sleep`, `setTimeout`, `cy.wait(2000)`, `page.waitForTimeout`) para esperar a trabajo asíncrono, en lugar de esperar a la condición real.
##Signs and Symptoms
Ves un retardo con número mágico colocado entre una acción y su aserción, con un comentario disculpándose por ello:
test('order is processed', async () => {
submitOrder(order);
// dale tiempo al worker para terminar
await new Promise((r) => setTimeout(r, 2000));
expect(await getOrderStatus(order.id)).toBe('processed');
});
Señales reveladoras:
- Sleeps fijos en cuerpos de prueba o en hooks:
Thread.sleep(500),await sleep(1000),time.sleep(2),cy.wait(3000),await page.waitForTimeout(2000),browser.pause(2000). - Comentarios como
// wait for the animation,// let the DB catch up,// flaky without this. - La duración del sleep deriva al alza con el tiempo (
1000se convierte en2000y luego en5000) a medida que la gente combate fallos intermitentes rellenando el retardo. - El número es la única sincronización: no hay aserción, sondeo ni evento al que la espera esté realmente vinculada.
- Una suite lenta donde el tiempo de reloj de pared está dominado por sleeps en lugar de por trabajo real.
El olor trata de la espera incondicional. cy.wait('@apiAlias') o await expect(locator).toBeVisible() esperan a una condición y están bien; cy.wait(2000) y page.waitForTimeout(2000) esperan al reloj y no.
##Reasons for the Problem
Por qué ocurre
- Una operación asíncrona (una cola, un temporizador, una animación, una llamada de red, un hilo en segundo plano) no tiene un punto de enganche obvio sobre el que esperar, así que un retardo es el camino de menor resistencia.
- Una prueba fallaba de forma intermitente y alguien la "arregló" insertando o alargando un sleep hasta que se puso en verde en su máquina.
- La prueba interactúa con un sistema externo real (reloj, planificador, sistema de ficheros, servicio HTTP) cuya finalización no es observable, así que se adivina el tiempo.
Por qué es perjudicial
- Fiabilidad / falsa confianza. Un sleep codifica una suposición: "el trabajo termina en N ms". El tiempo de procesamiento varía entre máquinas, según la carga de CI y entre ejecuciones, así que la prueba es no determinista: pasa en local y falla en un agente de CI cargado o, peor aún, el sleep es demasiado corto y la aserción compite con el código, pasando solo por suerte en el cronometraje. Es una de las causas raíz más citadas de las pruebas inestables (flaky).
- Lentitud. Un sleep fijo siempre espera toda la duración, aunque el trabajo terminase en 20 ms. Multiplicado por toda una suite, los sleeps convierten segundos de trabajo real en minutos de espera ociosa, lo que desincentiva ejecutar las pruebas a menudo.
- Mantenibilidad. El número mágico es frágil: acelerar o ralentizar el sistema bajo prueba rompe en silencio el contrato de tiempo, y el único "arreglo" al que recurre la gente es subir el número, un trinquete que vuelve la suite más lenta y aun así inestable.
- Legibilidad. El retardo oculta qué está esperando realmente la prueba. Un lector no puede saber si
sleep(2000)protege una escritura en BD, un renderizado o nada en absoluto, así que la intención de la prueba queda oscurecida.
##Treatment
Sustituye "esperar un tiempo fijo" por "esperar a la condición". Identifica la señal observable de que el trabajo asíncrono ha terminado y bloquéate sobre esa con un timeout generoso.
- Sondea la condición. Usa un helper de sondeo/reintento (Awaitility en la JVM,
vi.waitFor/waitForen Vitest o Testing Library, elwaitForde Jest, o las aserciones con reintento automático de tu runner) para que la prueba avance en el instante en que la condición es verdadera y solo falle tras un timeout. - Prefiere las aserciones con espera integradas. Las herramientas de E2E web ya reintentan: las aserciones web-first de Playwright (
await expect(locator).toBeVisible()) y los comandos con reintento automático de Cypress eliminan por completo la necesidad de esperar. - Espera a eventos, no al reloj. Aguarda la promesa, haz
awaitde la respuesta de red, o usa unCountDownLatch/callback/waitFor('@alias')que el código de producción señalice de verdad. - Controla el tiempo en lugar de gastarlo. Cuando el retardo es un temporizador real del código bajo prueba, usa temporizadores falsos (
jest.useFakeTimers(),vi.useFakeTimers(),vi.advanceTimersByTimeAsync) para avanzar el reloj de forma determinista en vez de dormir.
Antes:
test('order is processed', async () => {
submitOrder(order);
await new Promise((r) => setTimeout(r, 2000)); // sleepy
expect(await getOrderStatus(order.id)).toBe('processed');
});
Después (sondea la condición con un timeout acotado):
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 },
);
});
Equivalentes en Playwright/Cypress:
// Playwright: la aserción web-first reintenta automáticamente hasta que sea visible o expire el timeout
await expect(page.getByText('processed')).toBeVisible();
// Cypress: espera a la petición, no a un número
cy.intercept('POST', '/orders').as('createOrder');
cy.wait('@createOrder');
El resultado no espera más de lo necesario, falla rápido con un mensaje de timeout claro y ya no depende de la velocidad de la máquina.
##Detected by
- sonar java:S2925 — "Thread.sleep" should not be used in tests
- eslint-plugin-playwright playwright/no-wait-for-timeout — no-wait-for-timeout (disallow page.waitForTimeout)
- eslint-plugin-cypress cypress/no-unnecessary-waiting — no-unnecessary-waiting (disallow cy.wait with a number)
- eslint-plugin-ui-testing ui-testing/no-hard-wait — no-hard-wait (cy.wait, page.waitForTimeout, browser.pause, t.wait)