Eliminamos 37 tests y nuestra cobertura mejoró

Erik Treviño avatar
Erik Treviño
Cover for Eliminamos 37 tests y nuestra cobertura mejoró

Eliminamos 37 tests el mes pasado.

Nuestra cobertura mejoró.

No “se mantuvo igual” — mejoró. La suite corrió más rápido, tuvo menos flakes, y los tests WORKFLOW que quedaron ya cubrían cada camino que los tests eliminados verificaban. No perdimos ni una sola aserción significativa. Perdimos peso muerto.

Esto es algo que la mayoría de los equipos no van a admitir: una suite de tests que solo crece es como un codebase que solo crece. Eventualmente el costo de mantenimiento supera el valor. Todos los equipos tienen el instinto de “agreguemos más tests”. Casi ninguno tiene la disciplina de “auditemos lo que ya tenemos”.

El sistema de clasificación de cuatro etiquetas

Construí un clasificador de tests. No una herramienta de IA — una taxonomía. Una forma sistemática de examinar cada test en una suite y responder una pregunta: ¿Este test se gana su lugar?

Cada test recibe una de cuatro etiquetas:

  • WORKFLOW — Prueba un recorrido real del usuario de extremo a extremo. Login → navegar → realizar acción → verificar resultado. Estos son la columna vertebral. Se quedan.
  • FLUFF — Verifica algo que ya está cubierto por otro test en una mejor capa. El test de validación del formulario de login que también existe como test unitario. Se elimina.
  • MERGE — Dos tests que comparten el 80% de su setup y aserciones pero prueban ramas ligeramente diferentes. Se combinan en un solo test parametrizado.
  • KEEP_DETAILED — Un caso límite que genuinamente importa (manejo de zonas horarias, límites de permisos, rutas de migración de datos). Se queda, pero se evalúa si pertenece en la capa E2E o si podría moverse a integración/unitario.

La taxonomía es deliberadamente simple. Cuatro etiquetas. Sin escala de ambigüedad. Sin categoría de “tal vez”. Cada test recibe una clasificación definitiva.

Cómo se ve cada etiqueta en la práctica

WORKFLOW: un recorrido real del usuario

// WORKFLOW — This test earns its place. It validates a complete user journey
// that can only be tested through a browser.
test('user creates a report and shares it with a teammate', async ({ page }) => {
  await page.goto('/reports');
  await page.getByRole('button', { name: 'New Report' }).click();

  // Fill out the report form
  await page.getByLabel('Report Name').fill('Q1 Sales Summary');
  await page.getByRole('combobox', { name: 'Template' }).selectOption('quarterly');
  await page.getByRole('button', { name: 'Generate' }).click();

  // Wait for the report to generate (involves backend processing)
  await page.waitForResponse(resp =>
    resp.url().includes('/api/reports') && resp.status() === 201
  );
  await expect(page.getByText('Q1 Sales Summary')).toBeVisible();

  // Share with a teammate
  await page.getByRole('button', { name: 'Share' }).click();
  await page.getByLabel('Email').fill('teammate@company.com');
  await page.getByRole('button', { name: 'Send' }).click();
  await expect(page.getByText('Report shared successfully')).toBeVisible();
});

Este test ejercita navegación, envío de formularios, procesamiento backend, renderizado y una acción secundaria. Solo puede ejecutarse en un navegador. Valida una historia de usuario real. WORKFLOW.

FLUFF: ya cubierto en otro lugar

// FLUFF — This test validates form validation rules through the browser.
// The exact same validation logic is covered by unit tests on the
// validation schema. The E2E test adds 45 seconds of browser execution
// to verify something the unit test covers in 12 milliseconds.
test('report name field shows error when empty', async ({ page }) => {
  await page.goto('/reports');
  await page.getByRole('button', { name: 'New Report' }).click();
  await page.getByLabel('Report Name').fill('');
  await page.getByLabel('Report Name').blur();
  await expect(page.getByText('Report name is required')).toBeVisible();
});

Este test no está mal. La aserción es válida. Pero el test WORKFLOW de arriba ya navega a este formulario. Si la validación estuviera rota, el test workflow fallaría en el paso de envío del formulario. El test FLUFF agrega 45 segundos de tiempo de ejecución para re-verificar una regla de validación que un test unitario cubre en milisegundos.

MERGE: dos tests que deberían ser uno

// MERGE candidate A
test('admin can delete a report', async ({ page }) => {
  await loginAs(page, 'admin');
  await page.goto('/reports');
  await page.getByRole('row', { name: 'Q1 Sales' }).getByRole('button', { name: 'Delete' }).click();
  await page.getByRole('button', { name: 'Confirm' }).click();
  await expect(page.getByRole('row', { name: 'Q1 Sales' })).not.toBeVisible();
});

// MERGE candidate B — 90% identical setup, just checks a different element
test('admin sees delete confirmation dialog', async ({ page }) => {
  await loginAs(page, 'admin');
  await page.goto('/reports');
  await page.getByRole('row', { name: 'Q1 Sales' }).getByRole('button', { name: 'Delete' }).click();
  await expect(page.getByRole('dialog', { name: 'Confirm deletion' })).toBeVisible();
});

Estos dos tests comparten el mismo setup, la misma navegación, el mismo rol y la misma acción inicial. El test B es un subconjunto del test A. Se fusionan — el primer test ya hace clic en el diálogo de confirmación y verifica la eliminación.

Los resultados del audit

Apliqué esta taxonomía a una suite E2E de 180 tests. No en una sola pasada — tres rondas de clasificación con revisión manual en cada etapa.

EtiquetaCantidadPorcentajeAcción
WORKFLOW13474%Conservar — estos son la suite
FLUFF3721%Eliminar — cubiertos por otras capas o por tests workflow
MERGE21%Combinar en 1 test cada uno
KEEP_DETAILED74%Conservar en E2E, o migrar a la capa de integración

El 21% de la suite era FLUFF. Uno de cada cinco tests consumía tiempo de CI, contribuía a las tasas de flake y requería mantenimiento — sin aportar absolutamente nada de cobertura única.

El ciclo de auditoría en tres rondas

Una clasificación de una sola pasada no es confiable. Me perdí clasificaciones en la primera ronda, las detecté en la segunda y refiné las reglas de la taxonomía en la tercera.

Ronda 1: clasificación inicial. Aplicar las cuatro etiquetas a cada archivo de test. Usar análisis estático para identificar tests que no contienen acciones de navegación (probablemente FLUFF), tests que comparten más del 80% de sus locators con otro test (probablemente MERGE), y tests que solo verifican un solo elemento (probablemente FLUFF o KEEP_DETAILED).

# Quick static analysis: find tests that never call page.goto() or navigate
# These are often fluff tests that rely on another test's setup
grep -rL "page\.goto\|page\.click.*nav\|page\.getByRole.*link" tests/e2e/*.spec.ts

Ronda 2: revisión manual de cada etiqueta FLUFF. Para cada test etiquetado como FLUFF, responder dos preguntas: (1) ¿Existe un test WORKFLOW que ya cubra esta ruta? (2) ¿Un test unitario o de integración ya verifica este mismo comportamiento? Si ambas respuestas son sí, la etiqueta FLUFF se mantiene. Si alguna es no, reclasificar.

Ronda 3: refinar y re-auditar. Actualizar las reglas de clasificación con lo aprendido en la ronda 2. Volver a ejecutar el análisis estático. Verificar falsos negativos — tests etiquetados como WORKFLOW que en realidad deberían ser MERGE o KEEP_DETAILED.

Tres rondas. No una. El costo de un falso positivo (eliminar un test que aporta cobertura única) es mucho mayor que el costo de una ronda adicional de auditoría.

Antes y después

Rendimiento de la suite

MétricaAntes (180 tests)Después (141 tests)Cambio
Duración total de la suite24 min 12 seg17 min 45 seg-27%
Tasa de tests flaky3.2% de ejecuciones1.1% de ejecuciones-66%
Cobertura workflow única134 rutas134 rutasSin cambio
Horas de mantenimiento semanales~4 hrs~2.5 hrs-38%

La caída en la tasa de flake es la métrica más reveladora. Los 37 tests eliminados tenían la tasa de flake más alta de la suite — porque estaban probando detalles de implementación a través de la capa más frágil posible. Una regla de validación de formulario probada a través de un navegador es sensible al timing de renderizado, a los mutation observers del DOM y al manejo del focus. La misma regla probada en un test unitario tiene cero superficie de flake.

Lo que no perdimos

Este es el número que importa: 134 rutas workflow antes, 134 rutas workflow después. Cada recorrido real de usuario que se probaba antes de la auditoría sigue siendo probado después. No perdimos cobertura — perdimos redundancia.

La distinción importa. Cobertura que existe en dos capas (unitario + E2E) no es “más cobertura” que cobertura que existe en una sola capa correcta. Es costo duplicado con la misma protección.

Por qué se acumulan los tests FLUFF

Los tests FLUFF no son creados por ingenieros descuidados. Se acumulan por razones estructurales:

  1. Capas de testing faltantes. Cuando tu codebase no tiene una capa de tests de componentes ni una capa de tests de integración de API, el E2E se convierte en el cajón de sastre. Cada verificación de comportamiento, cada caso límite, cada “déjame solo verificar esta cosita” termina en E2E porque no hay otro lugar donde ponerlo.

  2. La suposición de “más tests = mejor”. Las métricas de velocidad del equipo que cuentan tests agregados por sprint incentivan la cantidad sobre la arquitectura. Nadie recibe crédito por eliminar un test, incluso cuando la eliminación mejora la suite.

  3. Creación de tests por copiar y pegar. Un desarrollador copia un test E2E existente como plantilla, cambia la aserción, y ahora hay dos tests que comparten el 90% de su setup. Ninguno está mal individualmente. Juntos, son candidatos a MERGE.

  4. Miedo a eliminar tests. “¿Y si lo necesitamos después?” Esto es el equivalente en testing del síndrome de acumulador. Si la cobertura existe en otra capa, eliminar el test E2E no elimina la red de seguridad — elimina el duplicado.

El protocolo de auditoría: lo que puedes hacer el lunes por la mañana

  1. Exporta tu lista de archivos de test. Cada archivo de test E2E de tu suite, uno por línea.

  2. Etiqueta cada test con su aserción principal. ¿Qué está verificando realmente este test? “El usuario puede iniciar sesión.” “Aparece un mensaje de error con entrada inválida.” “Los datos se cargan después de la navegación.”

  3. Cruza con tus tests unitarios y de integración. Para cada aserción E2E, ¿un test más rápido ya cubre el mismo comportamiento?

  4. Aplica las cuatro etiquetas. WORKFLOW, FLUFF, MERGE, KEEP_DETAILED.

  5. Revisa las etiquetas FLUFF con el equipo. No elimines unilateralmente. Muéstrale al equipo qué tests propones eliminar y qué tests workflow ya cubren esas rutas.

  6. Elimina en un solo PR con métricas de antes y después. Ejecuta la suite completa antes y después. Mide la duración, la tasa de flake y las rutas únicas cubiertas. Los números deberían hablar por sí solos.

El arte de la sustracción

Cada ingeniero sabe cómo agregar un test. Escribir el setup, escribir la aserción, ver que pase, hacer commit. Se siente productivo. El conteo de tests sube. El reporte de cobertura muestra verde.

Eliminar un test requiere un conjunto de habilidades diferente. Tienes que entender qué es lo que el test realmente cubre (no lo que dice su nombre), si esa cobertura existe en otro lugar de la suite, si la aserción pertenece a esta capa, y si eliminarlo crea un hueco o simplemente elimina un duplicado.

Eso es más difícil. Requiere entender la arquitectura completa de tests, no solo el archivo de test individual. Requiere la confianza de que tu suite restante es suficiente — confianza respaldada por análisis, no por esperanza.

Una suite ágil, centrada en workflows, con 141 tests que corren en 17 minutos, es más valiosa que una suite inflada con 180 tests que corre en 24 minutos y tiene el doble de flakes. La suite más pequeña es más rápida de ejecutar, más barata de mantener, más confiable de interpretar, y cubre exactamente los mismos recorridos de usuario.

Agregar tests es fácil. Eliminar tests es ingeniería.