Saltar al contenido principal
Engineering craft beyond tooling — design patterns, refactoring, code review, advanced testing strategy, and reading code you did not write.

Software Engineering Craft

Engineering craft beyond tooling — design patterns, refactoring, code review, advanced testing strategy, and reading code you did not write.

Testing Strategy Studio

Pyramid (classic)

Paso 0 / 0
Speed 100ms
Step 0 / 0
Phase —
Scenario —
Mut. score —
Status Ready
Shape breakdown unit / integration / E2E
Property / mutation / contract —
Step explanation

Pick a scenario and press Play to walk the testing-strategy loop.

—
Pseudocode
 

Estrategia de pruebas más allá de los tests unitarios

Intermediate (3/5) ~3 horas Pirámide vs panal vs trofeo de tests Pruebas basadas en propiedades (aleatorias + shrinking) Pruebas de contrato (guiadas por el consumidor) Mutation testing como oráculo de cobertura Snapshot tests y sus modos de fallo Legibilidad de los tests como preocupación de primer nivel Prereqs: Fundamentos de testing y debugging
Quick Reference

pyramid

No registry entry found for algorithm id "pyramid". If this is a curriculum-only studio, the complexity and quick-reference panel is intentionally omitted.

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

FormaMezcla recomendadaFallo del que advierte
Pirámide70% unit / 20% integración / 10% E2ESuites pesadas en E2E, lentas y flaky
PanalPocos unit / muchos integración / pocos E2EUnits repletos de mocks que pasan mientras la integración falla
TrofeoMuchos integración / algunos unit / pocos E2ERefactors 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 --updateSnapshot a 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, no renders 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 missing se lee mejor que testBinarySearch1. 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

  1. Toma una unidad de lógica pura y convierte un test por ejemplo en una propiedad con shrinking (prueba fast-check para JS, hypothesis para Python). Anota el primer bug que encuentre el fuzzer.
  2. Añade una corrida de mutation testing (stryker para JS, mutmut para Python) a un módulo crítico. Los mutantes sobrevivientes revelan líneas “cubiertas” que nadie testea realmente.
  3. Escribe un test de contrato guiado por el consumidor para un microservicio y un productor que lo verifique. Despliega ambos.
  4. Audita una suite pesada en snapshots: lee los últimos diez commits de --updateSnapshot. ¿Eran drift o cambio de comportamiento? Divide en cada categoría.
  5. 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ónConclusión
Lógica pura con casos límiteLas pruebas basadas en propiedades ganan a las basadas en ejemplos
Microservicios con historial de roturas en prodTests 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 testsComportamientos primero, mecánica después — un test flaky que pasa es peor que uno útil que falla