Resumen teórico
Imagina que construiste un robot de juguete. Antes de dárselo a un amigo, aprietas todos los botones para ver si camina, habla y gira la cabeza. Estás probando el robot. Una pruebauna pequeña verificación que ejecuta el programa y compara lo que hizo con lo que debería haber hecho en software es la misma idea: un pedacito de código que usa tu programa y dice 'sí, está bien' o 'no, está mal'.
¿Por qué importa esto? Los programas cambian todo el tiempo. Cada cambio puede arreglar una cosa y romper otra muy lejos. Si una computadora puede volver a revisar todo en pocos segundos, las personas no tienen miedo de tocar el código. Las apps, los bancos, los juegos y el software dentro de los autos solo se pueden mejorar con seguridad porque muchas pruebas vigilan todo, todos los días.
En este resumen vas a aprender: los distintos tamaños de prueba y por qué queremos muchas pequeñas y pocas grandes, cómo mantener una prueba sola y limpia, cómo usar ayudantes de mentira llamados dobles de prueba, el ritmo rojo-verde-refactorizar del desarrollo guiado por pruebas, cómo probar piezas que trabajan juntas y una app completa en el navegador, verificaciones rápidas de salud y verificaciones contra errores antiguos, cómo medir si tus pruebas son buenas, cómo la computadora puede inventar casos de prueba por ti, por qué algunas pruebas fallan por suerte, cómo elegir buenos valores de prueba y cómo las pruebas se ejecutan solas cada vez que alguien comparte código. Al final estarás listo para el quiz.
El panorama general: ¿para qué sirve una prueba?
Una prueba tiene tres partes. Primero preparas una situación. Luego haces la cosa que quieres verificar. Por último comparas el resultado con lo que esperabas. Si el resultado es distinto, la prueba falla y se avisa a un humano.
Palabras que vas a encontrar
- Defecto (bug): un error en el código que hace que el programa se comporte mal.
- Suite de pruebas: un grupo de pruebas que se ejecutan juntas, como un examen entero hecho de muchas preguntas.
- Pasar y fallar: una prueba pasa cuando el resultado es el esperado y falla cuando no lo es. Muchas herramientas muestran pasar en verde y fallar en rojo.
- Verificación: comprobar que el software hace lo que dice su descripción escrita (la especificación). La pregunta es: ¿construimos la cosa bien?
- Validación: comprobar que el software es lo que el cliente realmente necesita. La pregunta es: ¿construimos la cosa correcta? Un programa puede pasar la primera comprobación y fallar la segunda, por ejemplo cuando la propia descripción estaba equivocada.
- Prueba funcional: comprueba qué hace el sistema, por ejemplo 'para esta entrada, esta salida'.
- Prueba no funcional: comprueba cómo se comporta el sistema mientras lo hace, por ejemplo su rapidez, cuántos usuarios aguanta, su seguridad y su facilidad de uso.
Niveles de prueba y la pirámide de pruebas
Piensa en revisar un autito de juguete. Puedes mirar solo una rueda. Puedes mirar la rueda y el eje juntos. O puedes poner el autito entero en el piso y empujarlo. Cada revisión mira una pieza más grande. Con las pruebas es igual, y llamamos a cada tamaño un nivel de prueba.
| Nivel | Qué se ejecuta | Velocidad | Si falla, ¿dónde está el error? |
|---|---|---|---|
| Unitaria | Una pieza pequeña (una función o una clase), sola | Muy rápida | Fácil de encontrar: se ejecutaron pocas líneas |
| Integración | Dos o más piezas trabajando juntas, como el código más una base de datos real | Media | En la conexión entre las piezas |
| Sistema o de extremo a extremo (end-to-end) | La aplicación entera, usada como la usaría una persona | Lenta | En cualquier lugar: la prueba solo dice que algo se rompió |
| Aceptación | El cliente decide si el producto es suficientemente bueno | La más lenta | No se trata de errores: se trata de la necesidad del cliente |
La pirámide de pruebas es un consejo sobre cuántas pruebas de cada nivel escribir: muchas pruebas unitarias en la base ancha, menos pruebas de integración en el medio y poquísimas pruebas de extremo a extremo arriba. El motivo es el costo. Cuanto más alto el nivel, más lenta, más frágil y más cara es cada prueba.
la pirámide el cono de helado
/\ ___________________
/e2e\ pocas \ muchas e2e+manual /
/------\ \ pocas integr. /
/ integr.\ algunas \ poquísimas /
/----------\ \ unitarias /
/ unitarias \ muchas \_________/
/--------------\ lenta, inestable, difícil de depurarLa versión al revés es un error conocido, llamado cono de helado o pirámide invertida. Casi toda la verificación la hacen pruebas de extremo a extremo lentas y pruebas manuales. La suite tarda, falla por motivos que nada tienen que ver con el código y no dice dónde está el error. El remedio es empujar cada verificación hacia abajo, hasta el nivel más bajo que puede ver el comportamiento.
- unitaria
- 3.6 ms
- integración
- 7.6 ms
- extremo a extremo
- 690 ms
La aritmética de la pirámide
Supón una suite con 400 pruebas unitarias de 5 ms, 50 pruebas de integración de 200 ms y 10 pruebas de extremo a extremo de 6 segundos, ejecutadas una tras otra. El tiempo de cada nivel es el número de pruebas por la duración de una prueba.
- 400 pruebas unitarias
- 2 s
- 50 pruebas de integración
- 10 s
- 10 pruebas de extremo a extremo
- 60 s
La suite completa tarda 72 segundos, y las 10 pruebas lentas (una pequeña parte de todas) usan 60 de ellos, cerca del 83% del tiempo. Por eso no queremos una cima ancha.
¿Qué nivel ve qué error?
Una prueba solo encuentra errores que están en el camino que ejecuta. Un error en una regla de precio escrita como función simple lo ve una prueba unitaria. Un nombre de columna equivocado entre el código y la base de datos solo se ve cuando código y base se ejecutan juntos. Un botón cuyo clic está conectado al elemento equivocado de la página solo lo ve una prueba que abre la página, porque el código del servidor está perfecto.
Las tres etapas de Sommerville
- Pruebas de desarrollo: las hacen quienes construyen el sistema, para buscar defectos (pruebas unitarias, de componente y de sistema).
- Pruebas de versión (release): un equipo aparte prueba una versión completa para mostrar que cumple los requisitos y es suficientemente buena para usarse.
- Pruebas de usuario: usuarios o clientes reales prueban el sistema. En la prueba alfa prueban junto con el equipo. En la prueba beta una versión inicial va a voluntarios que la usan en su entorno real. En la prueba de aceptación el cliente decide si acepta el sistema.
Pruebas unitarias y aislamiento
Una prueba unitaria verifica una pieza pequeña del programa, como una función, sola. 'Sola' es la palabra clave. Para saber si una bombilla funciona, la pruebas sola, no dentro de toda la casa.
Una buena prueba unitaria es rápida (milisegundos), aislada (no depende de otras pruebas), repetible (mismo resultado siempre) y autoverificable (la propia prueba dice pasó o falló con aserciones, sin que una persona lea la salida y compare). Una suite autoverificable ejecuta miles de pruebas y tú solo miras las que fallaron.
Arrange, Act, Assert
test("un cupón de 10% baja 200 a 180", () => {
// Arrange: preparar la situación
const cart = new Cart([{ price: 200 }]);
// Act: hacer la única cosa que se prueba
const total = cart.totalWith("TEN");
// Assert: comparar con lo esperado
expect(total).toBe(180);
});Mantén un solo Act por prueba. Así, cuando la prueba falla, sabes exactamente qué comportamiento está mal.
La fixture y la familia xUnit
La fixturelos objetos y datos que una prueba necesita antes de poder ejecutarse es el escenario donde actúa la prueba. En la familia xUnit de herramientas (JUnit, pytest, Bun test y muchas más), setUp arma la fixture antes de cada prueba y tearDown la limpia después de cada prueba. Como cada prueba empieza en un escenario nuevo y no deja nada atrás, las pruebas pueden ejecutarse en cualquier orden, o una sola.
Un ejecutor de pruebas (test runner) también debe tratar una prueba que falló como un resultado normal. Captura el error de cada prueba, lo anota y sigue con la siguiente, de modo que una sola ejecución muestra todos los fallos a la vez. Un ejecutor que se detuviera en el primer error escondería los demás.
El estado compartido rompe el aislamiento
const cart: string[] = []; // compartido por todas las pruebas
test("adds an item", () => {
cart.push("book");
expect(cart.length).toBe(1);
});
test("adds a second item", () => {
cart.push("pen");
expect(cart.length).toBe(2); // solo pasa si la primera prueba corrió antes
});Ejecuta el archivo entero y queda verde. Ejecuta solo la segunda prueba en un proceso nuevo y falla, porque la lista empieza vacía, así que después de un push tiene 1 elemento y no 2. La señal clásica de estado compartido es una prueba que pasa en la suite completa pero falla sola o en otro orden.
Qué hace fácil de probar una unidad
- Función pura: para los mismos argumentos devuelve siempre el mismo valor y no cambia nada fuera de ella. No necesita reloj, números aleatorios, archivos ni red, así que solo das argumentos y revisas el valor devuelto. Pon la lógica en funciones puras y deja los efectos secundarios en una capa fina alrededor.
- [[Inyección de dependencias|entregar a un trozo de código lo que necesita desde afuera, en lugar de dejar que lo cree él mismo]]: si
registerUserhacenew SmtpMailer()por dentro, la prueba no puede impedir que se envíe un correo de verdad. Si recibe unmailercomo parámetro o en el constructor, la prueba le entrega un mailer de mentira. - Probar por la interfaz pública: revisa lo que la clase promete (valores devueltos, estado visible). Una prueba que lee un campo privado se rompe cuando el código se reescribe por dentro, aunque nada haya cambiado para los usuarios.
- Objetos con estado: el efecto de un método depende de los métodos llamados antes. Probar una clase
Connectioncon una prueba por método no basta: prueba también las secuencias, incluidas las inválidas, comosenddespués declose. Todas las líneas pueden estar cubiertas y la secuencia peligrosa no ejecutarse nunca.
Dobles de prueba: ayudantes de mentira
Una película usa un doble en las escenas peligrosas. En software, un doble de prueba es un objeto que ocupa el lugar de un ayudante real (una base de datos, un servidor de correo, un reloj) durante una prueba. Lo hacemos porque el ayudante real puede ser lento, costar dinero o ser imposible de controlar.
Las palabras de abajo vienen de Gerard Meszaros y las usa Martin Fowler. Son cinco tipos.
| Tipo | Qué hace | Ejemplo |
|---|---|---|
| Dummy | Solo llena un parámetro que la firma exige. Nunca se usa de verdad. | Un logger pasado al constructor en una prueba que no registra nada |
| Stub | Da respuestas preparadas a las llamadas que recibe. Controla las entradas indirectas del código probado. | Un servicio de cotización que siempre responde 5,2 |
| Spy | Un stub que además anota cómo lo llamaron. La prueba mira las anotaciones después de actuar. | Un mailer escrito a mano que guarda una lista sent |
| Mock | Recibe antes de que la prueba actúe qué llamadas esperar, y verifica por sí mismo. | Un objeto de biblioteca con 'send debe llamarse una vez con esta dirección' |
| Fake | Una versión real y funcional con un atajo que la hace inadecuada para producción. | Un repositorio que guarda los registros en un Map en memoria |
Verificación de estado y verificación de comportamiento
- Verificación de estado: la prueba usa la unidad y luego mira el resultado, el valor devuelto o el estado final de los objetos.
- Verificación de comportamiento: la prueba mira las llamadas hechas a los ayudantes. Es necesaria cuando el efecto es la propia llamada, como enviar un correo.
- El segundo estilo ata la prueba a cómo está escrito el código. Una prueba que solo cuenta llamadas puede pasar cuando la respuesta está mal y fallar después de una refactorización inofensiva. Cuando hay un valor devuelto o un estado final, la aserción principal debe ser sobre él.
Un fake suele dar pruebas más cortas y más fuertes que un mock cuando el ayudante tiene memoria. Si un servicio de registro guarda un usuario y después lo busca y lo actualiza, un repositorio fake deja que la prueba solo mire el usuario guardado al final. Un mock necesitaría una expectativa por cada llamada, lo que describe la conversación, no el resultado.
Usos que vale la pena conocer
- Situaciones difíciles de provocar: disco lleno, un servicio que responde 503, un timeout. Un doble configurado para fallar causa el error cuando lo pides, y así revisas cómo reacciona el código. El doble reemplaza al ayudante (el cliente HTTP), nunca a la unidad que se prueba.
- El tiempo como dependencia: el código que lee el reloj del sistema tiene una entrada escondida. Inyecta un
FakeClockque empieza en un instante fijo y solo avanza cuando la prueba llama aadvance. Así 'el token expira después de 15 minutos' se prueba en milisegundos, justo en el borde, y nunca falla por suerte. - Self Shunt: el nombre que da Kent Beck al caso en que la propia clase de prueba hace el papel del doble.
Desarrollo guiado por pruebas: rojo, verde, refactorizar
La mayoría de las personas escribe el código primero y la prueba después. El desarrollo guiado por pruebas (TDD, de test-driven development), creado por Kent Beck, lo da vuelta: escribes la prueba primero. Suena raro, pero es como dibujar la línea de meta antes de correr la carrera.
+--------+ hacerla pasar, rápido +--------+
| ROJO | ------------------------> | VERDE |
| prueba | | todas |
| nueva | | las |
| falla | <------------------------ | pasan |
+--------+ siguiente prueba chica +---+----+
^ |
| limpiar, mismo comportamiento v
+------------------------------ REFACTORIZAR- Rojo (red): escribe una prueba pequeña que falla.
- Verde (green): hazla pasar de la forma más rápida que puedas.
- Refactorizar (refactor): mejora la estructura y quita duplicación sin cambiar el comportamiento, con todas las pruebas aún pasando.
El TDD tiene dos reglas: escribe código nuevo solo cuando una prueba automatizada ha fallado y elimina la duplicación. Si surge una idea en medio del trabajo (por ejemplo 'este método debería rechazar números negativos'), primero se convierte en una prueba, o en una línea de tu lista de tareas. La lista de tareas es la memoria de corto plazo del TDD: escribes los casos que conoces, agregas los que aparecen en el camino y los tachas cuando pasan.
¿Por qué ver fallar la prueba primero?
El paso rojo prueba la propia prueba. Si una prueba nueva ya pasa antes de que exista el código, o falla por un motivo tonto como un error de tipeo o un import equivocado, no está verificando lo que querías. Solo después de verla fallar por la razón correcta la barra verde cuenta como prueba de que funciona.
Tres caminos al verde
| Estrategia | Qué haces | Cuándo sirve |
|---|---|---|
| Fake it (fingir) | Devuelves una constante que hace pasar la prueba y luego cambias la constante por variables hasta llegar al código real | Estás inseguro y quieres una barra verde ya |
| Triangulación | Esperas un segundo ejemplo antes de generalizar el código | Aún no conoces la abstracción correcta. Es la más cuidadosa |
| Implementación obvia | Escribes el código real directamente | La solución es clara. Es la más rápida |
Los pasos pueden ser grandes o pequeños. Cuando todo va bien, usa pasos grandes con implementación obvia. Si aparece un rojo inesperado, da pasos más pequeños (fake it, triangulación), porque un paso pequeño limita el lugar del error a pocas líneas.
Leer el historial de una sesión
Cada paso deja una firma en la suite. Rojo: se agrega una prueba nueva y la suite pasa de verde a rojo. Verde: cambia el código de producción y la suite pasa de rojo a verde. Refactorizar: cambia el código de producción, la suite está verde antes y después y no aparece ninguna prueba nueva. Nunca mezcles una refactorización con un cambio de comportamiento en el mismo paso: las pruebas que no cambiaron son la prueba de que el comportamiento se mantuvo, y si también editas los valores esperados, pierdes esa garantía.
El ejemplo del dinero
En el libro de Beck, Dollar y Franc se unen en una sola clase Money. Money es un objeto de valor (value object): se define por sus datos, no por su identidad, así que dos objetos con los mismos valores son iguales. Cuando las clases se unen, la moneda pasa a ser un campo, y equals debe compararla también.
equals(other: Money): boolean {
return this.amount === other.amount; // olvida la moneda
}
// ¿5 USD es igual a 5 CHF? Una prueba que lo pregunta falla y fuerza la corrección:
// return this.amount === other.amount && this.currency === other.currency;Beneficios y límites
Beneficios que se atribuyen al TDD: cobertura como subproducto, una suite de regresión que crece junto con el programa, depuración más fácil (el error está en las pocas líneas recién escritas) y pruebas que funcionan como documentación ejecutable. El límite: el TDD produce sobre todo pruebas unitarias escritas por el desarrollador, que revisa el código contra lo que él entendió. Formatos incompatibles entre servicios, lentitud bajo carga y la pregunta '¿el usuario necesita esto?' piden otros tipos de prueba.
Mira el video: TDD, Where Did It All Go Wrong (Ian Cooper)↑ Volver al índicePruebas de integración: piezas trabajando juntas
Dos piezas de un rompecabezas pueden ser perfectas y aun así no encajar. Una prueba de integración ejecuta dos o más módulos juntos, por su conexión real. El objetivo son los errores que solo existen en la frontera: parámetros intercambiados, formatos distintos, creencias equivocadas sobre el otro lado, configuración errónea. Las pruebas unitarias reemplazan a los vecinos por dobles y por eso nunca cruzan esa frontera.
Errores de interfaz
Sommerville separa los errores de una interfaz en tres clases.
| Clase | Significado | Ejemplo |
|---|---|---|
| Mal uso (misuse) | La llamada se hace mal: parámetros intercambiados, faltantes o del tipo equivocado | transfer(to, from, amount) en vez de transfer(from, to, amount) |
| Malentendido (misunderstanding) | Quien llama supone un comportamiento que la parte llamada no ofrece | Suponer que la lista vuelve ordenada |
| Temporización (timing) | Los dos lados trabajan a velocidades distintas y uno lee datos viejos | Un lector ve datos compartidos antes de que el escritor termine |
Cómo juntar las piezas
- Big-bang: juntar todos los módulos de una vez y probar el conjunto al final. Un fallo puede venir de cualquier interfaz, así que es muy difícil de localizar.
- Incremental: agregar un módulo a la vez y volver a ejecutar las pruebas, de modo que un fallo nuevo apunta a lo que acaba de entrar.
Mientras algunas piezas aún no existen, escribimos código de apoyo. Un driver llama al módulo probado en lugar de quien estaría encima. Un stub responde en lugar de quien estaría debajo. La integración bottom-up (de abajo hacia arriba) empieza con las capas de abajo reales y usa drivers. La top-down (de arriba hacia abajo) empieza con las capas de arriba reales y usa stubs. Imagina tres capas, UI sobre Service sobre Repository. Si el equipo construye de abajo hacia arriba y la UI aún no existe, escribe un driver que llame al Service.
[ driver ] --> [ Service ] --> [ Repository ]
código de prueba real real
(hace el papel de la UI que aún no se escribió)¿Base de datos real o fake?
Un fake en memoria es rápido, pero solo sabe lo que programamos en él. Una base de datos tiene reglas propias: claves únicas, claves foráneas, tipos, transacciones, el SQL real. Un repositorio que pasa con un Map puede romperse en producción con PostgreSQL cuando dos usuarios tienen el mismo correo. Por eso una suite saludable tiene algunas pruebas de integración contra la base real, o al menos el mismo motor.
Una base real es compartida, así que cada prueba debe empezar desde el mismo punto. Un truco común: abrir una transacción antes de cada prueba y deshacerla (rollback) después. Todo lo que la prueba escribió desaparece, sin el costo de reconstruir la base, y las pruebas no dependen del orden.
beforeEach(() => db.run("BEGIN"));
afterEach(() => db.run("ROLLBACK")); // olvida lo que la prueba escribió
// la prueba 1 inserta un pedido y ve 1 fila
// la prueba 2 corre después y vuelve a ver 0 filasUna prueba de integración también puede enviar una petición al handler HTTP dentro del mismo proceso y luego leer la fila en un SQLite real. Revisa toda la costura: el cuerpo se lee, se convierte, se pasa, se transforma en SQL y la tabla lo acepta. Cada capa puede pasar sus pruebas unitarias y el conjunto fallar igual por un nombre de columna distinto.
Pruebas de contrato
Supón que el servicio de pedidos se prueba contra un stub del servicio de pagos que responde { "total": 30 }, y el equipo de pagos renombra el campo a amount. Las pruebas de ambos equipos pasan y producción se rompe. Una prueba de contrato lo resuelve: lo que el consumidor espera se convierte en un archivo, y ese archivo se ejecuta contra el proveedor real en el pipeline del propio proveedor. Obtienes la verificación de la interfaz sin levantar todos los servicios juntos.
Pruebas de extremo a extremo con Playwright
Una prueba de extremo a extremo (e2e, de end-to-end) usa el sistema como lo usa una persona: por la pantalla, con todas las capas reales conectadas. Es la que da más confianza de que todo funciona junto. A cambio, es la más lenta, la más cara de mantener y la menos precisa sobre dónde está el error. Por eso va en la cima de la pirámide, en pocas unidades.
Playwright es una herramienta que controla un navegador real desde una prueba: abre páginas, hace clic en botones, escribe texto y lee lo que hay en pantalla.
Auto-waiting (espera automática)
Las páginas web no son instantáneas. Un botón puede estar deshabilitado mientras cargan los datos. Playwright espera por sí solo hasta que el elemento esté listo antes de actuar, así que la prueba no necesita pausas fijas como waitForTimeout(2000) (una pausa o es demasiado larga y pierde tiempo, o demasiado corta en una computadora lenta). Si el elemento nunca queda listo, la acción falla con un error de timeout.
Locators: encuentra las cosas como las ve el usuario
| Locator | Estilo | ¿Resiste un rediseño? |
|---|---|---|
getByRole("button", { name: "Checkout" }) | Lo que el elemento es para el usuario | Sí |
getByLabel, getByText | Lo que el usuario lee | Sí |
CSS o XPath largo como div > div:nth-child(3) .btn-x | Cómo el HTML está armado hoy por casualidad | No, se rompe en refactorizaciones que no cambian nada para el usuario |
Aserciones web-first y el await olvidado
Una aserción web-first como await expect(locator).toBeVisible() recibe el locator y repite la verificación hasta que pasa o se acaba el tiempo. Compárala con expect(await locator.isVisible()).toBe(true): recibe un verdadero o falso ya terminado, una foto de un instante. Si la pantalla aún no se actualizó, la foto dice falso y la prueba falla por suerte.
await page.getByRole("button", { name: "Add to cart" }).click();
// MAL: sin await, la promesa queda suelta y la prueba termina antes de la revisión
expect(page.getByTestId("cart-count")).toHaveText("1");
// BIEN:
await expect(page.getByTestId("cart-count")).toHaveText("1");Las aserciones de Playwright son asíncronas y devuelven una promise. Sin await, la prueba corre hasta el final sin esperar el resultado, y puede quedar verde aunque el contador muestre 0. Una prueba que no puede fallar es peor que ninguna prueba. Una regla de lint para promesas sueltas atrapa este error.
Aislamiento por contexto de navegador
Cada prueba recibe su propio contexto de navegador (browser context), que funciona como un perfil nuevo y vacío: sus propias cookies, localStorage y caché. Crear un contexto es barato. Así, si la primera prueba inicia sesión, la segunda prueba, que usa la fixture page, abre el sitio como un desconocido que no inició sesión. Las pruebas no dependen unas de otras ni del orden.
Controlar la red
page.route intercepta las peticiones que hace la página y deja que la prueba decida: responder con datos inventados, abortar o dejar pasar. Reemplazar un servicio de terceros por una respuesta fija hace la prueba estable e independiente de internet. Lo que deja de verificarse es la integración real con ese servicio, que pide otro tipo de prueba.
Pruebas de humo y de regresión
Cuando conectas una máquina nueva, primero revisas que no salga humo. Todavía no pruebas todas las funciones. Una prueba de humo (smoke test) es igual: un pequeño conjunto de verificaciones rápidas y superficiales de los caminos críticos. ¿La aplicación arranca? ¿Responde? ¿Funcionan las funciones esenciales? Se ejecuta justo después de un build o un deploy y actúa como un portero: si falla, la versión se rechaza al instante y nadie pierde tiempo con las suites largas.
Una regresión es la pérdida de un comportamiento que antes era correcto, causada por un cambio en algún lugar del código. La prueba de regresión consiste en volver a ejecutar las pruebas existentes después de cada cambio para ver que nada de lo que funcionaba dejó de funcionar. Con pruebas automatizadas es barato y puede hacerse en cada commit.
| Aspecto | Suite de humo | Suite de regresión completa |
|---|---|---|
| Profundidad | Superficial, muchas áreas de forma ligera | Profunda, en detalle |
| Tiempo | Segundos (por ejemplo 30 s) | Largo (por ejemplo 40 min) |
| Pregunta | ¿Vale la pena probar más esta versión? | ¿Todo lo que funcionaba sigue funcionando? |
| Cuándo | Justo después del build o deploy, primero | Después de que pasa la prueba de humo, y antes de un release |
Las suites se ordenan de la más barata a la más cara para que el fallo ocurra lo antes posible. La prueba de sanidad (sanity test) es un término que los equipos usan de maneras distintas, pero en general significa una verificación estrecha del área que acaba de cambiar.
Lo que entra en la suite de humo se elige por criticidad, no por profundidad: los caminos sin los cuales el sistema no sirve de nada (abrir el sitio, iniciar sesión, la operación principal). Los detalles de reglas, casos límite y errores antiguos van en las suites unitarias y de regresión. Si la suite de humo crece demasiado, pierde la rapidez que la justifica.
Cada error se convierte en una prueba
Un usuario reporta que un pedido con un cupón de 100% muestra un total negativo. ¿Qué debe hacer primero el desarrollador? Escribir una prueba que reproduzca el error y falle. Solo después arreglar el código y ver la prueba ponerse verde. El primer rojo demuestra que la prueba realmente ve el problema, y la prueba se queda para siempre protegiendo contra el regreso de ese error.
Cuando la suite de regresión es demasiado lenta
Si la suite completa tarda 3 horas, un equipo puede ejecutar en cada commit solo las pruebas de los archivos que importan los módulos cambiados. Eso da retroalimentación rápida, pero es una apuesta: las dependencias indirectas pueden hacer que un cambio rompa algo que parece lejano. La protección habitual es seguir ejecutando la suite completa a intervalos regulares y antes de cada release.
↑ Volver al índiceCobertura y pruebas de mutación
¿Cómo saber si tus pruebas son buenas? Una primera idea es contar cuánto del código ejecutan. Eso se llama cobertura (coverage).
| Tipo de cobertura | Qué cuenta |
|---|---|
| De líneas (o de sentencias) | Cuántas líneas fueron ejecutadas por al menos una prueba |
| De ramas (branch) | Si cada decisión (if) se tomó de las dos maneras: verdadera y falsa |
export function shippingFee(total: number, isMember: boolean): number {
let fee = 10;
if (isMember) fee = 5;
if (total >= 100) fee = 0;
return fee;
}
test("big member order is free", () => {
expect(shippingFee(150, true)).toBe(0);
});Esta única prueba pasa por todas las líneas, así que la cobertura de líneas es 100%. Pero las dos condiciones de los if solo son verdaderas. Cada if tiene dos ramas, así que son 4 ramas y la prueba recorrió 2: la cobertura de ramas es 50%. 100% de las líneas no significa 100% de las ramas.
Pruebas de mutación: probar las pruebas
La prueba de mutación hace la mejor pregunta: ¿las pruebas notarían si esta línea estuviera mal? Una herramienta hace copias del programa, cada una con un pequeño defecto plantado, llamado mutante. La herramienta cambia cosas como + por -, < por <=, una constante por otra, o elimina una sentencia. Luego ejecuta la suite sobre cada mutante.
programa original --- cambia una cosita ---> mutante
|
ejecuta la suite de pruebas
|
+--------------------------------+--------------+
| |
alguna prueba falla: MUERTO (killed) todas pasan: SOBREVIVIÓ
la suite notó el error la suite acepta el errorUn mutante que sobrevivió es una pregunta que las pruebas no hicieron: muestra dónde falta una aserción o una prueba.
El puntaje de mutación
El puntaje de mutación es los mutantes muertos divididos entre los mutantes que podían matarse. Un mutante equivalente tiene texto distinto pero se comporta exactamente como el original para cualquier entrada, así que ninguna prueba puede matarlo. Según la convención habitual, los mutantes equivalentes salen del denominador. Ejemplo: una herramienta genera 30 mutantes y la suite mata 24. De los 6 sobrevivientes, una persona decide que 2 son equivalentes. El puntaje es 24 / (30 - 2) = 24 / 28, cerca de 85,7%. Muchas herramientas no identifican los equivalentes y solo informan muertos sobre el total, así que di siempre qué convención se usa. Decidir si un mutante es equivalente es imposible en el caso general, por eso una persona suele revisar a los sobrevivientes.
if (weightKg > 2) { // mutante: weightKg >= 2
cents += (weightKg - 2) * 150; // en exactamente 2 kg suma (2 - 2) * 150 = 0
}Matar un mutante de frontera
Toma return age >= 18 y el mutante return age > 18. Solo difieren para la entrada 18. Una prueba solo mata al mutante si usa ese valor exacto y comprueba el resultado: isAdult(18) debe ser verdadero. Las pruebas con edades 10 y 30 dejan vivir al mutante. Esa es la razón práctica para probar los valores límite.
- suite débil: cobertura de líneas
- 100 %
- suite fuerte: cobertura de líneas
- 100 %
- suite débil: puntaje de mutación
- 21.1 %
- suite fuerte: puntaje de mutación
- 94.7 %
El costo de la prueba de mutación crece con el número de mutantes por el tiempo de la suite. Las herramientas lo reducen ejecutando, para cada mutante, solo las pruebas que cubren la línea modificada, deteniéndose en la primera prueba que falla, mutando solo el código cambiado desde la última ejecución y muestreando los mutantes.
↑ Volver al índicePruebas basadas en propiedades
Una prueba común es un ejemplo: 'ordenar [3,1,2] y obtener [1,2,3]'. Una prueba basada en propiedades enuncia una regla general, como 'ordenar nunca cambia el tamaño de la lista', y un generador inventa cientos de entradas aleatorias para intentar romper la regla. Encuentra casos en los que no pensaste. Pero solo comprueba las entradas que se generaron, así que no es una demostración.
Formas de propiedad
| Forma | Regla | Ejemplo |
|---|---|---|
| Ida y vuelta (round trip) | Ir y volver lleva al comienzo | decode(encode(x)) es igual a x |
| Invariante | Algo que la operación mantiene | Ordenar mantiene el mismo tamaño y los mismos elementos |
| Idempotencia | Hacerlo dos veces es lo mismo que una | normalize(normalize(s)) es igual a normalize(s) |
| Conmutatividad | El orden no importa | add(a, b) es igual a add(b, a) |
| Oráculo | Comparar con una versión simple y confiable | Un ordenamiento rápido contra uno lento pero obviamente correcto |
Shrinking (encogimiento)
Una entrada aleatoria que falla suele ser grande y llena de detalles inútiles, como una lista de 37 números. El shrinking busca por sí solo una entrada mínima que aún falla, por ejemplo [0, -1]. Un ejemplo pequeño hace fácil ver la causa del error.
La semilla (seed)
La aleatoriedad viene de un generador pseudoaleatorio, cuya secuencia entera la decide un solo número, la semilla (seed). Cuando una prueba de propiedades falla en CI, la herramienta imprime algo como seed: 48213. Ejecuta la prueba otra vez con la misma semilla y obtienes exactamente las mismas entradas. Sin esto, una prueba aleatoria sería una prueba flaky.
Una propiedad que no prueba nada
function average(a: number, b: number) { return Math.floor((a + b) / 2); }
for (let i = 0; i < 1000; i++) {
const a = randomInt(-100, 100), b = randomInt(-100, 100);
expect(average(a, b)).toBe(Math.floor((a + b) / 2)); // la misma fórmula
}El lado esperado repite la fórmula del código, así que la prueba pasa por construcción y no detecta nada. Una buena propiedad enuncia una relación exigida por la especificación e independiente del código, como 'el promedio queda entre el menor y el mayor de los dos números'.
¿Deben las propiedades reemplazar a los ejemplos? No. Las propiedades exploran muchas entradas, pero las buenas son difíciles de encontrar y son reglas generales, más débiles que un valor exacto. Los ejemplos documentan el comportamiento con números reales y garantizan que los casos límite conocidos siempre se ejecuten. Funcionan bien juntos.
↑ Volver al índicePruebas intermitentes (flaky)
Una prueba flaky (intermitente) pasa y falla con exactamente el mismo código. Nada cambió entre la ejecución verde y la roja. Ocurre cuando el resultado depende de algo que la prueba no controla: el reloj, el orden de las pruebas, el estado compartido, la red, la concurrencia o los números aleatorios. La señal típica es un fallo que desaparece cuando vuelves a ejecutar.
Ese es el daño real. El valor de una suite es la confianza: rojo significa error, verde significa que es seguro seguir. Las pruebas flaky rompen esto en los dos sentidos, generan falsas alarmas y enseñan al equipo a simplemente volver a ejecutar en lugar de investigar. Después de eso un error real puede pasar sin ser notado.
Las probabilidades pequeñas se suman
Si cada una de n pruebas independientes falla por suerte con probabilidad p, la suite pasa completa con probabilidad (1 - p) elevado a n. Entonces muestra al menos un fallo con probabilidad 1 - (1 - p)^n.
- 1 prueba flaky
- 2 %
- 10 pruebas flaky
- 18 %
- 50 pruebas flaky
- 64 %
Con 50 pruebas de 2% cada una, 0,98^50 es cerca de 0,364, así que 1 - 0,364 es cerca de 0,636. La suite queda roja casi dos ejecuciones de cada tres. Lo mismo pasa con una suite de 300 pruebas de extremo a extremo que fallan por suerte el 0,5% de las veces cada una: 0,995^300 es cerca de 0,22, así que alrededor del 78% de las ejecuciones muestran un fallo con el código correcto.
Las causas habituales y sus soluciones
| Causa | Ejemplo | Solución |
|---|---|---|
| Tiempo | La prueba lee el reloj dos veces y espera el mismo milisegundo | Inyectar un reloj falso con un instante fijo |
| Zona horaria | El código fecha en hora local, la prueba en UTC, y discrepan cerca de medianoche | Reloj falso, y comparar fechas en la misma zona. Probar a propósito un instante dentro de la ventana de riesgo |
sleep fijo | La prueba duerme 200 ms y revisa. En una máquina lenta el trabajo no terminó | Esperar la condición: esperar la promise o consultar con un timeout |
| Orden de los resultados | Trabajos concurrentes terminan en un orden distinto cada vez | Garantizar el orden en el código, o comparar los resultados ordenados |
| Estado compartido | Una prueba usa una caché que solo otra prueba llena | Cada prueba arma y restaura su propia fixture |
| Red real | El servicio tiene un mal momento | Usar un stub y probar el servicio real en una prueba de contrato aparte |
Mezclar el orden de las pruebas e imprimir la semilla es una forma de encontrar y repetir una dependencia de orden, no de curarla. Si B solo pasa cuando corre después de A, la cura es el aislamiento: B crea sus propios datos y limpia después de sí, para pasar solo y en cualquier posición.
Lo que no es una solución
- Reintentar hasta que quede verde: esconde el síntoma y mantiene la causa.
- Un sleep más largo: hace la carrera menos probable y la suite más lenta.
- Borrar u omitir la prueba sin entenderla: a veces el producto tiene una carrera real.
- Fijar el orden de las pruebas para que el estado compartido siga funcionando: la dependencia sigue ahí e impide ejecutar en paralelo.
Diseño de pruebas: clases de equivalencia y valores límite
No puedes probar todas las entradas posibles. Solo un campo numérico ya tiene miles de millones. Diseñar pruebas es el arte de elegir pocas entradas que dicen mucho.
Partición en clases de equivalencia
Divide las entradas posibles en grupos, llamados clases de equivalencia, en los que el programa debe tratar a todos los miembros de la misma manera. Luego elige al menos un representante de cada clase. Las clases salen de la especificación, no del código, e incluyen clases de entradas inválidas.
Contar las clases: una clase por cada rango que la especificación trata de forma distinta. Para una entrada numérica con tramos separados, son una clase por tramo válido, más una clase inválida por debajo del menor valor válido y otra por encima del mayor. Si una tabla de precios tiene 3 tramos de peso, más errores por demasiado liviano y demasiado pesado, son 3 + 2 = 5 clases y al menos 5 casos de prueba.
Análisis de valores límite
Los programadores se equivocan justo en los bordes: < en lugar de <=, un bucle que se detiene un paso antes. Un valor del medio de una clase pasa por el código defectuoso sin notarlo. Por eso, además del valor típico, prueba el borde y su vecino del otro lado.
rechazado | rango aceptado | rechazado
... -1 0 | 1 2 3 ... 18 19 20 | 21 22 ...
^ ^ ^ ^
a-1 a b b+1
valores de prueba: 0, 1, 20, 21La convención de este quiz: para un rango cerrado de números enteros [a, b], el método de dos valores prueba a-1, a, b y b+1. Imagina que el código dice grade >= 0 && grade < 10 cuando la especificación dice que las notas de 0 a 10 son válidas. Solo el valor 10 da una respuesta distinta de la especificación (falso en vez de verdadero). Todos los otros valores de prueba quedan del lado correcto, así que solo el valor en la frontera muestra el error.
Varias entradas: una inválida a la vez
Cuando una función tiene varias entradas, las clases válidas pueden combinarse en una sola prueba. Pero cada clase inválida debe ir sola, con las demás entradas válidas. Un programa normalmente se detiene en la primera verificación que falla. Si una prueba da una edad inválida y un ingreso inválido juntos, la verificación del ingreso nunca se alcanza, y nunca descubres que existe y funciona.
Pautas para secuencias
La experiencia dice dónde suelen romperse los programas. Para listas y otras secuencias: prueba la secuencia vacía, una secuencia con un elemento y tamaños distintos en pruebas distintas. Haz también que el elemento interesante aparezca primero, en el medio y al final. Para una función que devuelve el mayor número, buenas entradas incluyen [], [7], [9, 2, 3], [2, 9, 3] y [2, 3, 9].
Tablas de decisión
Cuando el resultado depende de varias condiciones de sí o no, una tabla de decisión lista las combinaciones y la acción esperada para cada una. Con n condiciones independientes de sí o no, la tabla completa tiene 2^n columnas, y cada columna se convierte en al menos una prueba. Si una condición decide sola, las columnas que difieren solo en las otras condiciones pueden unirse, escribiendo 'da igual' (don't care) para ellas.
| Columnas | Cuántas |
|---|---|
| Primera condición falsa: unidas en una columna con 'da igual' | 1 |
| Primera condición verdadera: las otras 3 condiciones dan 2 x 2 x 2 combinaciones | 8 |
| Total después de unir | 9 |
Pruebas en CI y estrategia de pruebas
Integración continua (CI) significa que todos comparten su trabajo con la línea principal con frecuencia, y cada aporte se verifica con un build y pruebas automáticos. Las pruebas se ejecutan en cada push, así que la retroalimentación llega rápido y apunta al cambio que se acaba de hacer. Cuando el build se rompe, arreglarlo va antes que el trabajo nuevo, porque todos dependen de la línea principal.
Ordena las etapas por costo
Un pipeline ordena sus verificaciones de la más barata a la más cara. Con fail-fast, la ejecución se detiene en la primera etapa que falla, así que un error que una verificación de 50 ms encuentra no espera detrás de una etapa de minutos.
push --> [ lint + tipos + unit ] --> [ integración ] --> [ e2e ] --> merge
segundos decenas de seg. minutos
| | |
falla: se detiene aquí falla: se detiene falla: se detieneUna prueba flaky bloquea a todos: ponla en cuarentena
Un pipeline solo es útil si rojo significa defecto. Supón que una prueba falla en 1 de cada 10 ejecuciones y bloquea merges, y el equipo no puede arreglarla hoy. Los malos caminos esconden el problema (reintentos silenciosos, borrar la prueba, debilitar sus aserciones) o mantienen el ruido (volver a ejecutar a mano). El buen camino es una cuarentena explícita: la prueba sale del camino bloqueante, la deuda se anota con un responsable y una fecha, y sigue ejecutándose en algún lugar para no olvidarla.
Estrategia basada en riesgo
No puedes probar todo, así que reparte el esfuerzo por el riesgo: probabilidad de que haya un defecto por su impacto. La probabilidad es mayor en código nuevo o muy cambiado, lógica compleja y partes con historial de fallos. El impacto es mayor cuando hay dinero, seguridad o muchos usuarios. La cobertura es solo una de las entradas.
| Módulo | ¿Cambió mucho? | Impacto de un defecto | Cobertura de líneas |
|---|---|---|---|
| Página 'Quiénes somos' | No | Texto equivocado en una página estática | 0% |
| Exportación de informe | No | 2 usuarios internos, una vez al mes | 40% |
| Cálculo de precio e impuesto | Sí, mucho | Cliente cobrado con un monto equivocado | 85% |
| Colores del tema del panel | Un poco | Apariencia de una página interna | 10% |
| Suscripción al boletín | No | No se envía un correo de marketing | 60% |
El módulo de precio e impuesto tiene el mayor riesgo: cambió mucho y un error cuesta dinero real. Debe recibir el esfuerzo aunque su cobertura ya sea la más alta, porque una cobertura alta no hace seguro a un módulo.
↑ Volver al índiceJuntando todo
Cada técnica responde una pregunta distinta. Un proyecto saludable usa varias, porque cada una es ciega a algo que otra ve.
| Pregunta | Herramienta |
|---|---|
| ¿Esta pieza pequeña funciona sola? | Prueba unitaria, con dobles para ayudantes lentos o incontrolables |
| ¿Las piezas encajan? | Prueba de integración, prueba de contrato |
| ¿Un usuario realmente puede hacer los recorridos principales? | Unas pocas pruebas de extremo a extremo con Playwright |
| ¿Vale la pena probar esta versión? | Prueba de humo |
| ¿Volvió un error antiguo? | Prueba de regresión, una por cada error encontrado |
| ¿Mis pruebas ejecutan el código? | Cobertura de líneas y de ramas |
| ¿Mis pruebas notarían un error? | Prueba de mutación |
| ¿Olvidé alguna entrada rara? | Pruebas basadas en propiedades, clases de equivalencia y valores límite |
| ¿Mis pruebas dicen la verdad? | Eliminar la flakiness: relojes falsos, aislamiento, stubs |
| ¿Alguien está rompiendo el código compartido? | CI con etapas ordenadas |
Errores comunes
- Poner toda verificación en pruebas de extremo a extremo (el cono de helado).
- Contar llamadas a dobles en vez de revisar el resultado.
- Confiar en un número de cobertura sin preguntar si las pruebas comprueban algo.
- Corregir un error sin escribir la prueba que lo reproduce.
- Dejar que las pruebas compartan estado o dependan del reloj, del orden o de la red.
- Refactorizar y cambiar el comportamiento en el mismo paso.
- Olvidar el
awaiten una aserción asíncrona.