Los tests unitarios verifican una función. Los tests de integración verifican el cableado. Los tests end-to-end verifican el resultado visible para el usuario. Una estrategia de pruebas saludable usa los tres, pero en proporciones distintas a las que sugiere la pirámide clásica — y añade una cuarta arma (basada en propiedades) que encuentra errores que los demás pasan por alto estructuralmente.
Pirámides, panales y trofeos
| Forma | Mezcla recomendada | Fallo del que advierte |
|---|---|---|
| Pirámide | 70% unit / 20% integración / 10% E2E | Suites pesadas en E2E, lentas y flaky |
| Panal | Pocos unit / muchos integración / pocos E2E | Units repletos de mocks que pasan mientras la integración falla |
| Trofeo | Muchos integración / algunos unit / pocos E2E | Refactors que rompen units pero no el comportamiento |
La verdad honesta: no existe una mezcla universal. Elige la forma que maximice la confianza por minuto de tiempo de test para tu codebase. Una librería es unit-pesada; un servicio CRUD es integración-pesado; una CLI suele ser E2E-pesada. Mide el tiempo de ejecución, mide las regresiones detectadas y luego权衡.
Pruebas basadas en propiedades
Un test unitario afirma un ejemplo. Un test basado en propiedades afirma un invariante y deja que un generador le lance miles de entradas. El test establece lo que siempre debe ser cierto; el harness encuentra la entrada que lo falsifica — y la reduce al menor reproductor posible.
propiedad: sort(list) preserva la longitud
AND sort(list) es monótona no decreciente
AND sort(sort(list)) == sort(list)
harness: 10.000 listas aleatorias de enteros, comprobar estos invariantes
Una propiedad que podría satisfacer un sort de base de datos: sort(xs ++ ys) == sort(sort(xs) ++ sort(ys)) para cada par de listas. Una propiedad que satisface un hash map: get(put(m, k, v), k) == v para cada map, clave y valor. El harness prueba decenas de miles de entradas; cuando una rompe la propiedad, la reduce a [] o [0, 0] — el caso mínimo que falla.
Los tests basados en propiedades capturan bugs off-by-one y de borde de dominio que los tests por ejemplo pasan por alto, porque nadie escribió un ejemplo para la entrada que rompía. Úsalos para lógica pura (parsers, sorts, máquinas de estado, transformaciones sobre estructuras algebraicas).
Pruebas de contrato (guiadas por el consumidor)
Un test de microservicio que mockea sus dependencias upstream/downstream puede pasar mientras la integración real se rompe en el momento en que un productor despliega. Un test de contrato lo soluciona: cada consumidor escribe sus expectativas contra un contrato compartido; cada productor verifica que satisface todos los contratos antes de desplegar.
El consumidor escribe: "Llamo a POST /users con {name, email}; espero 201 con {id} o 409"
El productor verifica: pasa todos los contratos antes de poder desplegar
Este es el decodificador del antipatrón “tenemos tests pero la integración sigue rompiéndose en prod”. Herramientas como Pact automatizan esto; la práctica de ingeniería es anterior a las herramientas. El principio es: el consumidor es dueño del contrato, no el productor.
Mutation testing
La cobertura te dice qué líneas se ejecutaron. El mutation testing te dice si tus tests fallan cuando tu código cambia — una medida mucho más verdadera.
La herramienta: toma cada línea de código de producción, aplica mutaciones pequeñas (+ → -, == → !=, if (x) → if (true)) y vuelve a correr los tests. Cada mutación que sobrevive es un agujero — un cambio de código que nada atrapa.
código de producción: if (i < list.length)
mutación: if (i <= list.length)
¿pasan los tests? sí → mutante sobreviviente → la cobertura mintió
no → mutante atrapado → el test habría atrapado la regresión
El mutation testing es caro. Úsalo como auditoría periódica, no como gate por commit: una sola corrida sobre módulos críticos expone qué líneas “cubiertas” tienen realmente un test de comportamiento vigilándolas.
Snapshot tests: baratos y frágiles
Un snapshot test captura la salida serializada de un componente (típicamente: árbol React renderizado, respuesta JSON de API, HTML generado) y la re-afirma en cada corrida. Barato de escribir, caro de mantener, porque cada refactor actualiza el snapshot haya cambiado o no el comportamiento.
Reglas de fallo de los snapshots:
- Los snapshots deben revisarse en los diffs. Un
--updateSnapshota ciegas es el modo de fallo; convierte el test en un no-op. - Los snapshots deben nombrar lo que están probando:
renders signup form with validation errors, norenders correctly. - Los snapshots no son aserciones — son detectores de cambio. Su valor está en su drift, no en su existencia.
Úsalos con moderación para probar estabilidad de salida; prefiere tests basados en propiedades para lógica pura, prefiere tests de integración para comportamiento.
Legibilidad de los tests
Un archivo de tests se lee más a menudo de lo que se escribe, igual que el código de producción. Tres hábitos componen:
- Nombra los tests por comportamiento, no por método:
returns -1 when target is missingse lee mejor quetestBinarySearch1. El nombre es tu documentación. - Arrange / Act / Assert (AAA): cada test tiene la misma forma de tres secciones, separadas por líneas en blanco. Las desviaciones son ruidosas.
- Una aserción por test es una heurística, no una ley — la ley real es “un comportamiento por test”. Tres post-condiciones relacionadas afirman una sola cosa.
Un mutante en if (x > 0) return compute(x) debería matar exactamente un test, no tres.
Trayectoria de práctica
- Toma una unidad de lógica pura y convierte un test por ejemplo en una propiedad con shrinking (prueba
fast-checkpara JS,hypothesispara Python). Anota el primer bug que encuentre el fuzzer. - Añade una corrida de mutation testing (
strykerpara JS,mutmutpara Python) a un módulo crítico. Los mutantes sobrevivientes revelan líneas “cubiertas” que nadie testea realmente. - Escribe un test de contrato guiado por el consumidor para un microservicio y un productor que lo verifique. Despliega ambos.
- Audita una suite pesada en snapshots: lee los últimos diez commits de
--updateSnapshot. ¿Eran drift o cambio de comportamiento? Divide en cada categoría. - Refactoriza un test que viole AAA en tu propio código a forma AAA. Describe qué hizo visible la nueva forma.
Cuándo es la herramienta correcta
| Situación | Conclusión |
|---|---|
| Lógica pura con casos límite | Las pruebas basadas en propiedades ganan a las basadas en ejemplos |
| Microservicios con historial de roturas en prod | Tests de contrato guiados por el consumidor |
| “Cobertura al 90% pero los bugs siguen saliendo” | El mutation testing revela líneas cubiertas solo de nombre |
| Salida generada (HTML, JSON, JSX) | Snapshot tests como detectores de cambio, no como aserciones |
| Revisando un PR de tests | Comportamientos primero, mecánica después — un test flaky que pasa es peor que uno útil que falla |