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.
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.
No compra corrección: compra el derecho a cambiar el código sin miedo. Un equipo sin pruebas no es valiente, es lento.
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.
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.
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.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.
| Herramienta | Qué se mira | Có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.
“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?
Por qué: una regla de negocio sobrevive a los rediseños; el número de efectos y el tamaño en píxeles cambian con cada refactor. Esa prueba se rompe por decisiones de implementación, no porque el producto falle.
La suite está en verde, pero dos pruebas fallan una de cada diez corridas. ¿Cuál es el daño principal?
Por qué: el tiempo se recupera; la costumbre no. Cuando “vuelve a correrlo” es la respuesta normal, el semáforo deja de informar y el equipo pierde el único canal que le avisaba de una regresión.
El backend renombra fecha_entrega a delivery_date. ¿Qué lo detecta antes de producción?
Por qué: el mock guardado a mano conserva el nombre viejo y el snapshot también: los dos te dan la razón. Solo algo que hable con el contrato vigente nota que el campo desapareció, y por eso la prueba se ejecuta contra el esquema publicado o contra el servidor de pruebas.
¿Qué presupuesto de tiempo es razonable para la suite que corre en cada pull request?
Por qué: el presupuesto no es una preferencia: es la condición para que la suite se use. Pasados unos minutos, la gente empuja y espera; pasada la hora, la corre al día siguiente y el error llega cuando ya hay tres cambios encima.