ConstructiCat Logo
CodeBust.
Browse section ▾

Test errático (inestable).

Un test errático (inestable) pasa y falla de forma intermitente sobre el mismo código porque su resultado depende del tiempo, el orden, el estado compartido u otros factores no deterministas en lugar del comportamiento bajo prueba.

##Signs and Symptoms

Un test que está verde en una ejecución y rojo en la siguiente sin ningún cambio de código es errático. Lo reconoces tanto por el comportamiento humano a su alrededor como por el código: los desarrolladores reejecutan el CI "para que pase", añaden @Flaky/retry(3) o ponen el test en cuarentena en lugar de arreglarlo.

Señales habituales en el código y en el patrón de fallo:

  • Temporización/asincronía: "esperas" con sleep/setTimeout, promesas sin esperar (await) o aserciones que compiten con el sistema bajo prueba. Los fallos se correlacionan con la velocidad de la máquina o la carga del CI.
  • Dependencia del orden: el test pasa en aislamiento pero falla en la suite completa, o viceversa. Barajar el orden de los tests cambia el resultado.
  • Estado mutable compartido: colecciones estáticas, una fila de BD reutilizada, un singleton o un fixture mutado por un test anterior.
  • Entradas no deterministas: Date.now()/new Date(), Math.random(), configuración regional/zona horaria, orden de iteración de un hash-map o IDs autogenerados.
  • Recursos externos: red, sistema de archivos o reloj reales. Los fallos parecen timeouts o "connection refused", no discrepancias de aserción.
  • Aserciones condicionales: expect oculto dentro de if/catch/callbacks, de modo que el test pasa en silencio cuando la rama nunca se ejecuta.
// Olor: una "espera" fija, un arreglo compartido y un reloj real
const created = [];                       // compartido entre tests → tests que interactúan

test('shows a fresh receipt', async () => {
  render(<Checkout />);
  fireEvent.click(screen.getByText('Pay'));
  await new Promise(r => setTimeout(r, 300));   // se espera que la petición haya terminado
  created.push('order-1');                       // se filtra a tests posteriores
  expect(screen.getByRole('status'))
    .toHaveTextContent(`Paid ${new Date().toISOString()}`); // cambia en cada ejecución
});

Esto se corresponde directamente con los subolores del Test errático de Meszaros: Tests que interactúan / Guerra de ejecución de tests (estado compartido), Test solitario (solo se ejecuta tras otro), Optimismo de recursos (asume que un recurso externo está ahí), Fuga de recursos (no limpia) y Test no determinista / irrepetible (tiempo, aleatoriedad, asincronía).

##Reasons for the Problem

Por qué ocurre

  • Temporización implícita. El código asíncrono se "sincroniza" con sleep fijos o sin esperar (await) en absoluto. El retardo es una conjetura: suficiente hoy, demasiado corto mañana bajo carga.
  • Acoplamiento oculto. Los tests comparten una base de datos, un singleton, variables a nivel de módulo o archivos en disco. Los efectos secundarios de un test se convierten en las precondiciones de otro (los Tests que interactúan / Guerra de ejecución de tests de Meszaros), así que los resultados dependen del orden y de lo que se ejecutó en paralelo.
  • Se cuelan entradas no deterministas. El tiempo real del reloj, Math.random(), la configuración regional/zona horaria y las colecciones sin orden varían entre ejecuciones y máquinas.
  • Dependencia optimista del entorno. Los tests acceden a una red/servicio en vivo o asumen que un archivo existe (Optimismo de recursos) y nunca liberan lo que adquieren (Fuga de recursos).
  • Aserciones que pueden saltarse. Poner expect dentro de un condicional o de un .catch significa que la comprobación puede no ejecutarse nunca, así que el test "pasa" por accidente.

Por qué hace daño

  • Falsa confianza y defectos enmascarados. Un test inestable puede fallar por razones ajenas a producción, y una regresión real puede esconderse tras un fallo que todos asumen que es "solo inestabilidad". Ya no puedes distinguir la señal del ruido.
  • Erosión de la confianza. Una vez que se sabe que una suite es inestable, los desarrolladores ignoran las compilaciones en rojo y reintentan por reflejo, lo que entrena al equipo para desestimar la suite de tests por completo.
  • Tiempo desperdiciado y pipelines rotos. Los reintentos, las reejecuciones y las investigaciones de "¿soy yo o es el test?" ralentizan a todos y bloquean el CI/CD por cuestiones que no lo son.
  • Mantenibilidad pobre. Los tests erráticos son difíciles de depurar porque el fallo no es reproducible; tienden a desactivarse en vez de arreglarse, reduciendo en silencio la cobertura real.

##Treatment

Trata la inestabilidad como un defecto del test, no como una rareza que sortear con reintentos. Los reintentos sirven para detectar la inestabilidad, nunca para ocultarla. Pon el test en cuarentena si bloquea el pipeline y luego busca la causa raíz.

1. Reemplaza los sleeps por espera basada en condiciones. Sondea el estado que de verdad te importa en lugar de adivinar una duración.

// antes — retardo fijo y propenso a carreras
fireEvent.click(screen.getByText('Pay'));
await new Promise(r => setTimeout(r, 300));
expect(screen.getByRole('status')).toBeInTheDocument();

// después — espera la condición y luego comprueba
fireEvent.click(screen.getByText('Pay'));
expect(await screen.findByRole('status')).toBeInTheDocument();

2. Haz deterministas las entradas. Inyecta el reloj o usa temporizadores falsos; siembra o haz stub de la aleatoriedad; fija la configuración regional/zona horaria.

// antes: depende de la fecha real
expect(label).toBe(`Paid ${new Date().toISOString()}`);

// después: congela el tiempo (vitest/jest)
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-06-15T00:00:00Z'));

3. Aísla cada test. Da a cada test fixtures nuevos, reinicia el estado compartido (BD, singletons, globales de módulo) en beforeEach/afterEach y evita las variables mutables a nivel de módulo. Ejecuta la suite en orden aleatorio (p. ej. --seed/randomize de Jest, MethodOrderer.Random de JUnit) para sacar a la luz las dependencias de orden cuanto antes.

4. Haz stub de los recursos externos. Simula la red, el sistema de archivos y las llamadas al sistema para que el test nunca dependa de que un servicio esté disponible (cura el Optimismo de recursos); libera/limpia siempre los recursos adquiridos (cura la Fuga de recursos).

5. Espera todo el trabajo asíncrono y comprueba sin condiciones. Devuelve/espera (return/await) las promesas y las utilidades de test asíncronas; saca los efectos secundarios de los callbacks de waitFor para que se ejecuten una sola vez; nunca entierres expect dentro de if/catch.

6. Verifica el arreglo. Ejecuta el test ahora determinista muchas veces (y bajo carga / con el orden barajado) para confirmar la estabilidad antes de sacarlo de cuarentena.

##Detected by