Saltar al contenido principal

El Trofeo del Testing 🏆

Trofeo ≠ TDD. El Trofeo define qué tipo y proporción de pruebas tener; TDD define el ritmo con que se escriben (la prueba antes que el código). Son complementarios: en Wiedii las pruebas se escriben guiadas por TDD y la suite resultante se distribuye según el Trofeo.

Qué es y de dónde viene

El Testing Trophy es un modelo de distribución de esfuerzo en testing propuesto por Kent C. Dodds en 2018, como evolución del Testing Pyramid clásico de Martin Fowler (2012). Fue inspirado por el tweet de Guillermo Rauch (creador de Socket.io, fundador de Vercel):

"Write tests. Not too many. Mostly integration."

La idea central: el retorno sobre la inversión (ROI) de los tests no es uniforme. Escribir más tests no da necesariamente más confianza — importa qué tipo de tests escribes y en qué proporción.


El Trofeo vs. la Pirámide

AspectoPirámide (Fowler, 2012)Trofeo (Dodds, 2018)
Capa más grandeUnit testsIntegration tests
Premisa principalLos E2E son lentos y caros — evítalosLas herramientas mejoraron; el ROI cambia
Incluye análisis estáticoNoSí (base de la pirámide)
FocoVelocidad de ejecuciónConfianza real por inversión de tiempo
Contexto originalBackend / lenguajes compiladosFrontend / JavaScript / TypeScript

La Pirámide asume que los E2E son prohibitivamente lentos y costosos. Esa premisa era cierta en 2012 con Selenium. Con Playwright y Vitest Browser Mode en 2025, ya no lo es.

El principio guía del Trofeo:

"The more your tests resemble the way your software is used, the more confidence they can give you." — Kent C. Dodds


Estructura del Trofeo

El Trofeo tiene cuatro niveles (análisis estático, unit, integration, E2E). Las técnicas como property-based y contract testing no son niveles — son técnicas transversales que se aplican dentro de los niveles unit e integration (ver sección Técnicas transversales).


/E\ ← pocos, lentos, máxima confianza
/2E \
/─────\
/ \
/ Integr. \ ← LA MAYOR INVERSIÓN
/ tests \
/─────────────\
/ Unit tests \ ← rápidos, bajo costo, confianza acotada
/─────────────────\
/ Static analysis \ ← TypeScript, ESLint — sin costo de ejecución
────────────────────

Nivel 0 — Análisis estático (base, costo cero en runtime)

TypeScript y ESLint detectan errores antes de ejecutar cualquier código. No son "tests" en sentido estricto pero proveen la primera capa de confianza sin costo de ejecución. En Wiedii son obligatorios en todos los proyectos JS/TS.

Qué captura: errores de tipos, variables no usadas, imports mal formados, violaciones de convención. Qué NO captura: lógica de negocio incorrecta, interacciones entre módulos.

Nivel 1 — Unit tests (base del trofeo)

Prueban una única función, clase u objeto en aislamiento total — sus dependencias están mockeadas o ausentes.

Cuándo son valiosos:

  • Lógica pura (funciones matemáticas, parsers, transformadores de datos)
  • Algoritmos con muchos casos borde (binary search, validaciones complejas)
  • Utilidades independientes reutilizadas en múltiples lugares

Cuándo NO son la respuesta:

  • Cuando la función A llama a la función B — un unit test de A con B mockeada no prueba que A+B trabajen juntas
  • Cuando el comportamiento que importa es la interacción entre capas

Regla práctica: antes de escribir un unit test, pregunta: ¿un integration test que ya existe cubre este path? Si sí, el unit test es redundante.

Nivel 2 — Integration tests (el corazón del trofeo — LA MAYOR INVERSIÓN)

Prueban múltiples unidades interactuando entre sí — con tan poco mocking como sea posible.

Son el núcleo del Trofeo porque ofrecen el mejor balance entre:

  • Velocidad de escritura y ejecución (mucho más rápidos que E2E)
  • Confianza real (prueban comportamiento observable, no implementación)
  • Resistencia al refactor (si cambias cómo está implementado algo pero el comportamiento es el mismo, el test no debe romperse)

Qué mockear en integration tests:

  • Servicios externos reales (APIs de terceros, pasarelas de pago) — usar MSW u otro interceptor HTTP
  • Efectos secundarios costosos (emails, notificaciones push)

Qué NO mockear:

  • Tu propia lógica de negocio
  • Tu propia base de datos (usar una instancia de prueba real o en memoria)
  • Tus propios módulos internos

Señal de que tienes demasiados mocks: si tu integration test mockeó 5 cosas para probar una función, probablemente es un unit test disfrazado.

Nivel 3 — End-to-End tests (cima del trofeo)

Prueban el sistema completo sin mocks — exactamente como lo usaría un usuario real. Desde la UI hasta la base de datos, pasando por toda la pila.

Son los tests que dan más confianza por definición, pero históricamente eran los más costosos. En 2025 esto cambió: con Playwright, los E2E son significativamente más rápidos y confiables que con Selenium. Kent C. Dodds ha planteado públicamente que en algunos contextos modernos (SSR, full-stack frameworks), el E2E podría convertirse en la capa más grande del trofeo.

Qué cubrir con E2E:

  • Los flujos críticos de negocio — login, checkout, onboarding, operaciones irreversibles
  • Happy paths de las features más importantes
  • Flujos que cruzan muchos servicios y donde una integration test no puede cubrir la integración real

Qué NO cubrir con E2E:

  • Casos borde — son lentos y costosos para eso; los cubren los integration tests
  • Cada posible estado de UI — usa integration tests con estados simulados

Cómo elegir: árbol de decisión unit vs integration

La decisión más frecuente —y la que más se equivoca— es entre unit e integration. Este árbol la resuelve:

¿Qué estás probando?

├─ Una función pura, sin dependencias (parser, cálculo, transformador, validador)
│ └─→ UNIT test.
│ Si tiene muchos inputs posibles → añade un property test (fast-check).

├─ Una función que llama a OTROS módulos/funciones tuyos
│ └─→ INTEGRATION test. No mockees tus propios módulos.

├─ Lógica que toca la base de datos o un servicio interno
│ └─→ INTEGRATION test con DB de prueba real (o en memoria). No mockees tu DB.

├─ Un endpoint / handler HTTP completo
│ └─→ INTEGRATION test. Mockea solo servicios externos (ej. con MSW).

├─ La frontera entre dos paquetes del monorepo (productor ↔ consumidor)
│ └─→ INTEGRATION test + contract testing (ver Técnicas transversales).

└─ Un flujo de usuario de punta a punta (login, checkout, onboarding)
└─→ E2E test (Playwright), solo si es un journey crítico.

Regla de desempate: ante la duda entre unit e integration, elige integration. Solo baja a unit cuando (a) la lógica aislada es genuinamente compleja y (b) un integration test ya cubre el path de interacción. Un unit test que duplica lo que un integration ya verifica es deuda, no cobertura.


Técnicas transversales (no son niveles)

Property-based y contract testing son técnicas, no capas del trofeo. No tienen un nivel propio: se aplican dentro de los niveles unit e integration cuando aportan valor. Por eso no aparecen en el diagrama ni reciben un porcentaje fijo de esfuerzo.

Property-based testing (fast-check)

Los tests de propiedad prueban invariantes que deben sostenerse para cualquier input dentro de un dominio, no para inputs concretos hardcodeados. Refuerzan unit e integration tests cuando existe lógica con muchos valores de entrada posibles — no los reemplazan.

Wiedii usa fast-check, compatible tanto con bun:test como con Vitest.

import * as fc from 'fast-check';
import { describe, test, expect } from 'bun:test';

describe('serialize / deserialize — invariante de ida y vuelta', () => {
test('siempre recupera el valor original', () => {
fc.assert(
fc.property(fc.record({ id: fc.uuid(), name: fc.string() }), (obj) => {
expect(deserialize(serialize(obj))).toEqual(obj);
}),
);
});
});

Cuándo aplicar property tests:

  • Funciones de serialización / parseo — propiedad: deserialize(serialize(x)) === x
  • Operaciones conmutativas o asociativas
  • Funciones de ordenamiento — propiedad: el resultado siempre está ordenado
  • Validadores — propiedad: inputs válidos nunca se rechazan, inputs inválidos nunca se aceptan
  • Cualquier función donde "para todos los X válidos, Y debe cumplirse"

Cuándo NO aplicar property tests:

  • Comportamientos con semántica específica por caso (reglas de negocio que cambian por país, por tipo de usuario, etc.)
  • Flujos de UI — ahí van integration o E2E tests

Contract testing (fronteras entre paquetes y servicios)

El contract testing verifica que el productor y el consumidor de una API coinciden en la forma del contrato (request/response, tipos, campos obligatorios). Es una técnica de nivel integration: cubre la frontera entre dos componentes que evolucionan por separado.

Es especialmente relevante en nuestro monorepo NX, donde los paquetes dependen unos de otros a través de interfaces. Cuando un paquete compartido cambia su API, el contract test detecta qué consumidores se rompen antes del deploy.

Cómo aplicarlo en Wiedii:

  • Paquetes TS internos del monorepo → el contrato es el tipo exportado. TypeScript (tsc --noEmit) + un integration test en la frontera del paquete suelen ser suficientes. No se necesita herramienta dedicada.
  • Servicio ↔ servicio por HTTP (cuando se despliegan independientemente) → considerar Pact para contratos consumer-driven.
  • En NX, ejecutar los tests de frontera solo sobre lo afectado: nx affected -t test evita correr toda la suite en cada cambio. Ver nx-monorepo.

Cuándo NO hace falta: si productor y consumidor se versionan y despliegan siempre juntos (mismo release del monorepo), el integration test ya cubre el contrato — no añadas Pact.


Propiedades de un buen test — F.I.R.S.T.

Independiente del nivel (unit, integration, E2E) y del framework, un test debería cumplir cinco propiedades — el acrónimo F.I.R.S.T. (Robert C. Martin). No son niveles ni técnicas: son la calidad que hace que una suite se mantenga útil en lugar de convertirse en lastre.

PropiedadQué exigeSeñal de que se viola
Fast (rápido)La suite corre en segundos, no minutos; si es lenta, se deja de correrTests que duermen (sleep), pegan a servicios reales en cada caso, o levantan todo el stack para probar lógica
Independent (independiente)Cada test corre solo y en cualquier orden; no depende del resultado de otroTests que comparten estado global, se rompen al reordenarlos o exigen ejecutarse en secuencia
Repeatable (repetible)Mismo resultado siempre, en cualquier entorno, sin conexión externaFlaky tests: dependen de la fecha/hora, de la red, de datos de producción o de un orden de hashing
Self-validating (auto-validable)El test decide solo si pasa o falla, con aserciones; no requiere inspección manual"Tests" que imprimen a consola y esperan que un humano lea la salida
Timely (oportuno)Escrito en el momento adecuado — idealmente antes del código, vía TDDEl test llega semanas después, cuando el diseño ya se osificó y cuesta probarlo

Checks rápidos: ¿la suite tarda más de unos segundos en local? (Fast) · ¿pasa si la corres en orden aleatorio? (Independent) · ¿pasa sin red y dos veces seguidas igual? (Repeatable) · ¿falla sola, sin que nadie lea logs? (Self-validating) · ¿se escribió junto con el código, no mucho después? (Timely).

La "T" de Timely es el puente con tdd: TDD garantiza por construcción que el test llega a tiempo (primero el rojo). Las otras cuatro aplican a todo test de la suite, lo hayas escrito con TDD o no.

La estructura interna de cada test (Arrange-Act-Assert) está en tdd — F.I.R.S.T. es la calidad, AAA es la forma del cuerpo del test.


Estado actual en la industria (2025-2026)

El Trofeo es la referencia más citada en el ecosistema JavaScript/TypeScript. El panorama actual:

ArquitecturaModelo más adoptado
MonolitosTesting Pyramid
MicroserviciosTesting Honeycomb (Spotify)
Serverless / FaaSTesting Trophy
Full-stack JS/TSTesting Trophy

Ningún modelo es universalmente correcto. El consenso actual en la industria:

  • Para lógica de dominio compleja (reglas de negocio, algoritmos, cálculos) → más unit tests, la Pirámide funciona bien
  • Para aplicaciones orientadas a API, SSR o flujos de usuario → el Trofeo es superior
  • La mayoría de proyectos exitosos usan un híbrido: muchos integration tests como columna vertebral, unit tests donde la lógica es compleja, E2E para journeys críticos

La pregunta que se está debatiendo activamente en 2025-2026 (incluido el propio Kent C. Dodds): con Playwright y el browser mode de Vitest haciendo los E2E tan baratos como los integration tests, ¿deberían los E2E convertirse en la mayor inversión? La respuesta aún no es definitiva, pero la tendencia apunta a que los E2E van a crecer en proporción relativa a medida que las herramientas mejoran.


Cómo aplica en Wiedii

Por tipo de proyecto

Tipo de proyectoModeloInversión principal
Web app / API / SSRTesting TrophyIntegration tests
CLI tool pura (sin DB, sin web)Pyramid adaptadoUnit tests + integration de comandos
Librería/paquete reutilizablePyramidUnit tests exhaustivos
Monorepo con múltiples capasTrophy por capaIntegration entre paquetes + contract testing en fronteras

Distribución orientativa del esfuerzo

E2E → ~10-15% (journeys críticos, no casos borde)
Integration → ~60-70% (EL NÚCLEO — aquí va la mayor inversión)
Unit → ~15-20% (lógica compleja aislada)
Static → 100% (TypeScript + ESLint en todo el código)

Técnicas transversales (NO son un nivel, no tienen % propio):
· Property-based (fast-check) → invariantes de lógica con muchos inputs
· Contract testing → fronteras entre paquetes/servicios (NX)

Estos porcentajes son orientativos — no son reglas fijas. El objetivo es la confianza, no alcanzar un número.

Framework por tipo de proyecto

Tipo de proyectoFramework de testingNotas
CLI tools (sin DB, sin web)bun:test + node:testDoble flujo cross-runtime: garantiza ejecución en Bun y en Node — ver bun-testing
Web apps / APIs / full-stackVitest (decisión provisional — ver abajo)Ver bun-testing para la política completa
Property-based (cualquier tipo)fast-check + el runner del proyectoCompatible con bun:test y Vitest
Contract (HTTP, servicios independientes)PactSolo si productor/consumidor se despliegan por separado
E2EPlaywrightEstándar de facto en 2025

Decisión provisional: Vitest para web/API

⚠️ Decisión PROVISIONAL — sujeta a confirmación. Estado: Vitest es el framework por defecto provisional para web apps, APIs y proyectos full-stack (todo lo que tenga DB o servidor web). bun:test queda acotado a CLI tools puras. Por qué provisional: falta validar en un proyecto real de Wiedii la compatibilidad de Vitest con nuestra pila (Node como runtime, bindings nativos como sharp) y la madurez de su browser mode frente a Playwright. Deadline de decisión final: 2026-09-30 (fin Q3 2026). Antes de esa fecha hay que: (1) probar Vitest en un proyecto piloto, (2) medir tiempos de ejecución y DX vs. bun:test, (3) confirmar o revertir esta decisión en bun-testing y en esta nota. Mientras tanto: proyectos nuevos de web/API usan Vitest; no se migra ningún proyecto existente hasta la decisión final.

Adoption path para proyectos legados

El Trofeo describe el estado ideal, no un mandato de reescribir suites existentes de golpe. Para un proyecto legado sin tests, o con una distribución desbalanceada (típicamente: muchos unit tests con mocks excesivos y cero integration), la adopción es incremental:

  1. Empieza por el análisis estático. Activa TypeScript en modo estricto y ESLint. Coste bajo, valor inmediato, cero tests que escribir. Es la base del trofeo y la primera red de seguridad.
  2. No persigas coverage retroactivo. Intentar cubrir todo el código viejo de una vez es trabajo enorme con poco retorno. Aplica la regla del campamento: todo código que toques para una feature o fix, lo dejas con al menos un integration test que cubra su comportamiento.
  3. Cada bug se convierte en un test. Cuando llegue un bug a producción, escribe primero un test que lo reproduzca (regression test), luego arréglalo. La suite crece donde el sistema realmente falla.
  4. Prioriza los flujos de negocio críticos. Añade integration tests primero a los caminos que más duelen si se rompen (cobros, autenticación, datos irreversibles), no al código más fácil de testear.
  5. E2E solo para los 2-3 journeys intocables. No intentes E2E de todo el legado; cubre los flujos que no pueden romperse jamás.
  6. Migra, no reescribas. Si hay una suite vieja de unit tests con mocks excesivos, no la borres de golpe: déjala correr y ve sustituyéndola por integration tests a medida que tocas cada zona. Retira los unit redundantes solo cuando un integration ya cubre ese path.

La meta no es "100% de cobertura del legado" — es detener la sangría: que cada cambio nuevo entre con la distribución correcta y que cada bug deje un test atrás.

Qué NO hacer

Demasiados unit tests con mocks excesivos — el error más común al intentar "cubrir" código. Un test que mockea 80% de las dependencias de una función da falsa confianza: el código puede pasar los tests y romperse en producción porque la integración real falla.

E2E para todo — la cima del trofeo es pequeña por una razón. Usar E2E para probar cada campo de formulario o cada mensaje de error hace los tests frágiles, lentos y difíciles de mantener.

100% de cobertura de código como objetivo — la cobertura es un indicador, no el objetivo. El 100% puede lograrse con tests que no detectan bugs reales. El objetivo es la confianza en que el sistema funciona.


El principio que lo resume todo

"Write tests. Not too many. Mostly integration." — Guillermo Rauch / Kent C. Dodds

Tres instrucciones en ocho palabras:

  1. Write tests — sí, los tests salvan tiempo y ahorran llamadas a las 2am
  2. Not too many — los rendimientos decrecen pasado cierto punto; 100% de coverage no es el objetivo
  3. Mostly integration — esta es la inversión con mejor ROI: prueban comportamiento real, resisten refactors, son más rápidos que E2E y más confiables que unit tests aislados

Referencias