Lección 13

¿Cuántas pruebas hay que escribir para dormir tranquilo?

Una prueba es una frase que la computadora comprueba cada vez que alguien toca el código. La pregunta no es cuántas necesitas, sino cuáles puedes correr en el tiempo que la gente está dispuesta a esperar.

01Qué es una prueba y para qué existe

Una prueba es una frase que la computadora comprueba sola: prepara un caso, ejecuta el código y compara el resultado con el esperado. “Pagar 300 con un cupón de 50 deja 250” se ejecuta en cada cambio, sin que nadie tenga que acordarse.

Una prueba no demuestra que el código esté bien: demuestra que un comportamiento que te importaba sigue igual después de tu cambio. El código se rompe porque alguien lo cambió para arreglar otra cosa.

La cobertura mide qué líneas se ejecutaron, no qué se comprobó. Un archivo con 100% de cobertura y ninguna aserción (ninguna comparación con el resultado esperado) se ve impecable y no detecta nada.

La suite es la lista del piloto antes de despegar: doce puntos, cuatro minutos, en el mismo orden. No vuela el avión; hace que volver a volar mañana sea una decisión y no una apuesta. Si un punto fallara al azar una de cada diez, el piloto aprendería a saltárselo.

Lo que compra una prueba

No compra corrección: compra el derecho a cambiar el código sin miedo. Un equipo sin pruebas no es valiente, es lento.

02Por qué sucede: la pirámide y las pruebas que mienten

La pirámide de pruebas reparte el esfuerzo: mucha base de pruebas unitarias (una función o un módulo, sin navegador), una capa media de componente (una pieza de interfaz montada con su DOM) y una punta de pruebas E2E (end to end: la aplicación completa en un navegador real). La versión honesta no es una ley, es una proporción: del orden de 70% abajo, 20% en medio y 10% arriba. La punta no debe comerse el presupuesto.

Cada nivel de la pirámide se paga distinto E2E ~5 s componente ~120 ms dominio puro ~2 ms cuántas ~20 ~150 ~800 lento y caro: 5 s por prueba mantenimiento alto rápido y estable: 2 ms aquí rinde más cada minuto
La base vale porque cada prueba tarda milisegundos y falla por la lógica, no por un selector; la punta se reserva para los caminos que pagan.

Probar el dominio puro es lo que más rinde: funciones que reciben datos y devuelven datos, como un total, un permiso o una fecha. No tocan DOM, red ni reloj, corren en milisegundos y no dan falsos rojos: fallan por la regla, no por un selector. Cuando una se pone roja, el mensaje dice qué fecha y qué zona fallaron.

En componente se prueba lo que la persona ve y hace: que el botón tenga nombre accesible, que el error aparezca junto al campo, que Escape cierre el diálogo. Se monta la pieza con su DOM y cada prueba tarda decenas o cientos de milisegundos.

En E2E se prueba el viaje completo: entrar, filtrar, pagar, ver el comprobante. Es lo único que junta red, rutas y despliegue, y también lo más caro de mantener; un E2E que falla por un aviso de cookies no dice nada del producto. La regresión visual compara capturas y detecta lo que el DOM no ve, como un texto encimado.

Lo que no conviene probar: valores exactos de CSS, funciones privadas, cuántas veces se llamó un hook, el HTML completo de una pantalla. Si se rompe al renombrar una variable y no al cambiar el comportamiento, mide tu código.

Las pruebas inestables (flaky) pasan a veces y fallan a veces sin cambios. Las causas se repiten: tiempo (esperar con setTimeout, el reloj real, animaciones a medias), red (peticiones de verdad, respuestas en otro orden), orden (una prueba deja datos que otra espera), datos compartidos y azar sin semilla. Son peores que no tener pruebas: la que no existe no promete nada; la inestable enseña a repetir hasta el verde, y ese hábito repite también el rojo real.

El reporte en verde esconde dos corridas en rojo lo que reporta la CI 142 passed 0 failed 48 s todo en verde 8 corridas de la misma suite 1 2 3 4 5 6 7 8 la 3 y la 7 fallaron solas el reintento automático las tapó el costo no es el tiempo: es que el rojo deja de significar algo
Las mismas 142 pruebas, ocho corridas. El reporte solo conserva la última; las dos rojas se reintentaron sin que nadie leyera el error. La siguiente vez que algo falle de verdad, también se reintenta.
El contrato ve el cambio; el mock guardado lo esconde el backend renombra fecha_entrega API delivery_date esquema vigente campo nuevo prueba de contrato falla antes de producción mock a mano copia de ayer fecha_entrega test con snapshot pasa igual y no detecta nada el mock copia una verdad que ya cambió; el contrato la vuelve a leer
Un mock escrito a mano es una foto del contrato de ayer: sigue diciendo fecha_entrega y pasa. La prueba de contrato habla con el esquema o la respuesta vigente, y por eso el renombre se convierte en un build rojo.

03Cómo se ataca: dominio, contrato, datos y presupuesto

El primer movimiento es sacar el cálculo del componente. Si la regla de la ventana de fechas vive en un hook que lee el reloj, solo se prueba montando la pantalla; si vive en una función que recibe el instante y la zona, se prueba en milisegundos y se reutiliza en el servidor. Eso es inyectar dependencias.

// ventana.js — dominio puro: sin DOM, sin red, sin reloj.
export function diaLocal(instante, zona) {
  return new Intl.DateTimeFormat('en-CA', {
    timeZone: zona,
    year: 'numeric', month: '2-digit', day: '2-digit',
  }).format(new Date(instante)); // YYYY-MM-DD
}

export function sumarDias(fecha, dias, zona) {
  const [a, m, d] = diaLocal(fecha, zona)
    .split('-').map(Number);
  // Se suma sobre el calendario, no sobre ms:
  // 24 h no siempre son un día (el 3 de noviembre
  // en Nueva York dura 25; el 10 de marzo, 23).
  const t = Date.UTC(a, m - 1, d + dias, 12);
  return new Date(t).toISOString().slice(0, 10);
}

// ventana.test.js — los casos borde son el motivo.
import { describe, expect, test } from 'vitest';
import { diaLocal, sumarDias } from './ventana';

describe('diaLocal', () => {
  test('cruza la medianoche de la zona, no UTC', () => {
    // 02:30 UTC del 11 son las 20:30 del 10 en CDMX.
    expect(
      diaLocal('2024-03-11T02:30:00Z',
        'America/Mexico_City'),
    ).toBe('2024-03-10');
  });

  test('Tijuana y el centro no ven el mismo día', () => {
    // La frontera sí cambia; el centro no, desde 2022.
    expect(
      diaLocal('2024-11-04T07:30:00Z',
        'America/Tijuana'),
    ).toBe('2024-11-03');
    expect(
      diaLocal('2024-11-04T07:30:00Z',
        'America/Mexico_City'),
    ).toBe('2024-11-04');
  });
});

test('sumar un día no son 24 horas', () => {
  // 23:30 del 9 de marzo en Nueva York:
  // 24 h después del instante ya es el 11.
  expect(
    sumarDias('2024-03-10T04:30:00Z', 1,
      'America/New_York'),
  ).toBe('2024-03-10');
});

// La versión mala: pasa con una función que devuelve basura.
test('sumarDias no truena', () => {
  expect(() =>
    sumarDias('2024-03-10T04:30:00Z', 1,
      'America/New_York'),
  ).not.toThrow();
});

Esa última prueba es verde aunque sumarDias devuelva la misma fecha: solo comprueba que no lanza una excepción. “No truena” describe un accidente, no un comportamiento.

El segundo frente es el contrato entre frontend y API. El backend renombra fecha_entrega a delivery_date, cambia un null por una cadena vacía o manda otro formato, y te enteras en producción. TypeScript no cruza la red: los tipos se borran al compilar. Una prueba de contrato valida la respuesta real (o el esquema publicado) con un esquema en la frontera, tipos generados desde OpenAPI o un contrato del consumidor con Pact. Un mock escrito a mano conserva el nombre viejo; el contrato falla cuando el otro lado cambia.

En regresión visual el problema es la tolerancia. Demasiado estricta, cualquier diferencia de antialiasing o de fuente produce cientos de imágenes y el equipo aprende a aprobar sin mirar. Demasiado laxa, un umbral del 5% deja pasar un botón desplazado 40 px en una página de 2.000. Se arregla controlando la entrada: misma ventana, fuentes del repositorio, animaciones apagadas, reloj congelado y máscaras sobre avatares, anuncios y horas relativas.

Los datos de prueba necesitan aislamiento. Una base por worker (cada proceso con su esquema, desde una plantilla) evita que dos pruebas se pisen; semillas deterministas (ids fijos, azar con semilla, reloj congelado) hacen que el mismo caso dé el mismo resultado. Si dos pruebas comparten el mismo correo o la misma fila, el orden decide.

Y está el presupuesto. La suite que corre al guardar debería tardar un par de segundos; la de cada pull request, menos de diez minutos; la completa, de noche. Si tarda cuarenta, la gente la corre el viernes y los errores llegan el lunes. Se sostiene dividiendo por proyectos y repartiendo en procesos.

04Cómo diagnosticarlo

HerramientaQué se miraCómo se ve el síntoma
--repeat / --rerun-each La misma suite 20 veces, con orden aleatorio. Falla en la corrida 7 y pasa en la 8: inestable confirmada; si nunca falla, el azar no era la causa.
Tiempo por archivo El reporte detallado o el XML de la CI. 40 s en un archivo y 2 s en el resto: un E2E disfrazado de unitario.
Cobertura (v8, Istanbul) Líneas cubiertas y aserciones por archivo. 92% de líneas con pruebas que no comparan nada: cobertura alta, señal nula.
Aserciones ausentes expect-expect de ESLint o mutación con Stryker. Pasa una prueba que solo llama a la función; con mutación, el mutante sobrevive.
Orden aleatorio --sequence.shuffle o el orden por defecto del runner. Verde en orden alfabético y rojo al mezclar: estado compartido entre archivos.
Reintentos en CI Cuántas pruebas pasan solo al segundo intento. Con retry: 2 global, el reporte dice cero y nadie sabe cuántas fueron.

Si el 80% del tiempo está en cinco archivos E2E, no faltan pruebas: sobran E2E y falta dominio. Y si el reporte lleva semanas en verde sin que nadie haya visto un rojo, conviene preguntarse si las pruebas miran algo.

Para decirlo en la entrevista

“Most of our tests live at the domain level: pure functions, no browser, milliseconds per test.”

“We run contract tests in CI, so a renamed backend field fails the build instead of production.”

“Flaky tests get quarantined within a day; a suite nobody trusts is worse than no suite.”

Comprueba que lo tienes

Cuatro preguntas. Al comprobar se marca la lección como completada.

¿Cuál de estas pruebas probablemente siga siendo útil dentro de un año?

La suite está en verde, pero dos pruebas fallan una de cada diez corridas. ¿Cuál es el daño principal?

El backend renombra fecha_entrega a delivery_date. ¿Qué lo detecta antes de producción?

¿Qué presupuesto de tiempo es razonable para la suite que corre en cada pull request?