Saltar al contenido principal
Avanzado IACódigoTesting

Prompts para Detectar Regresiones con IA: Mutation Testing y Snapshots

10 prompts para evitar y detectar regresiones con IA: mutation testing con Stryker, snapshot testing, caracterización de legacy y tests de regresión visual.

Fran Cobos 26 min de lectura 5062 palabras

Tabla de contenidos

Un lunes abres el panel de Deploys. El build de ayer pasó los 214 tests en verde. Nadie ha tocado el código de facturación desde el martes. Y sin embargo, desde ayer los descuentos del segundo artículo de un carrito se aplican al primero.

Ese es el escenario del que nadie habla en las reuniones de testing. La regresión silenciosa: el código cambió de comportamiento y los tests pasaron igual. Porque los tests no afirmaban lo que creías que afirmaban. Este artículo es sobre eso, y sobre cómo usar IA para convertir tests que no dicen nada en tests que sí.

He visto el mismo patrón en cuatro empresas distintas. El CI verde, los tests “pasan”, y un ticket de negocio diciendo que “algo no cuadra desde el martes”. Nunca es el bug más obvio. Es un > que se convirtió en >=, un && que se convirtió en ||, o un return que alguien puso en el sitio equivocado de un if.

Qué es una regresión silenciosa y por qué pasa

Un test útil hace una afirmación que puede fallar. Un test que pasa siempre no hace ninguna afirmación. La cobertura mide líneas ejecutadas, no el comportamiento comprobado. Y esa línea se llama mutation score, no “cobertura de tests”.

Piensa en esto:

// El código
function puedeAplicarDescuento(pedido) {
  return pedido.total > 100 && pedido.usuario.verificado;
}

// El test
it('aplica descuento', () => {
  expect(puedeAplicarDescuento({ total: 150, usuario: { verificado: true } })).toBe(true);
});

Si mañana alguien cambia > por >=, el test sigue pasando. Si cambia && por ||, sigue pasando. Si lo pone al revés, sigue pasando. El test verifica que un pedido de 150€ verificado devuelve true. Eso es todo. El 90% de la función no está verificado.

Eso no es un test malo. Es un test tautológico, y es lo que los modelos de lenguaje generan por defecto cuando les pides “escribe tests para esta función”.

Reglas antes de meter IA en los tests

  1. Nunca dejes que la IA escriba el test y la implementación en el mismo mensaje. Si el mismo modelo los genera, ambos tienen el mismo error y se validan mutuamente. Es el principio más importante de toda la lista.
  2. Empieza por caracterizar, no por probar lo que debería hacer. En legacy, un test que documenta el comportamiento actual es más útil que uno que falla contra lo que tú crees que hace.
  3. Un snapshot no es una regresión detectada, es un snapshot guardado. El valor está en el diff revisado, no en el fichero .snap.
  4. Prioriza por coste del bug, no por porcentaje de cobertura. Un 95% en un CRUD y un 40% en el cálculo de impuestos es un riesgo exactamente invertido.
  5. Si un test es flaky, arréglalo antes de añadir más. Un 5% de falsos positivos en CI entrena al equipo a ignorar los rojos.

1. Convertir tests tautológicos en tests reales con mutation testing

Este es el prompt central del artículo. Stryker genera mutantes del código y ejecuta tus tests contra cada mutante. Si el mutante sobrevive (el test sigue pasando), tu test no verifica esa línea.

Actúa como ingeniero de QA especializado en mutation testing. Voy a darte una función y sus tests.
Tu trabajo es encontrar todos los cambios mutantes que TUS TESTS NO DETECTARÍAN.

Función:
[pega la función completa]

Tests existentes:
[pega todos los tests que la cubren]

Para cada línea y cada operador, enumera:
1. El mutante concreto (qué cambia exactamente)
2. Si mis tests lo detectarían: SÍ / NO / QUIZÁS
3. Si la respuesta es NO o QUIZÁS, el test exacto que necesito añadir para detectarlo
4. Para los cambios de límite (>, >=, <, <=): el caso de prueba exacto en valor absoluto,
   no "un caso más"

Quiero el resultado en formato tabla y después el código de los tests que faltan,
en el framework que ya estoy usando ([Vitest / Jest]).

No me des tests que repitan la lógica de la implementación. Cada test debe poder
fallar aunque la implementación sea distinta.

Por qué funciona: mutar el código mentalmente es una tarea que los modelos hacen bien, y requiere exactamente el tipo de razonamiento que hacen bien: enumerar casos de frontera a partir de una estructura de condicionales.

Ejemplo real de salida para la función del principio:

MutanteDetectadoTest que falta
> → >=NOexpect(puedeAplicarDescuento({ total: 100, usuario: { verificado: true } })).toBe(false)
&& → ||NOexpect(puedeAplicarDescuento({ total: 150, usuario: { verificado: false } })).toBe(false)
total > 100 → total < 100NOexpect(puedeAplicarDescuento({ total: 150, usuario: { verificado: true } })).toBe(true)
verificado eliminadoSÍ— (ya hay un caso con verificado: false)
return true hardcodeadoSÍ—

Tres mutantes sobreviven. Añades tres asserts y el score sube sin tocar producción.

2. Configurar Stryker en un proyecto real

Ahora el prompt para la infraestructura. Stryker se integra con Vitest y Jest, y genera un informe HTML que puedes subir como artefacto de CI.

Configura Stryker (mutation testing) en mi proyecto:

Stack:
- Framework de tests: [Vitest]
- Runner: [npm / pnpm]
- Lenguaje: [JavaScript con JSDoc / TypeScript]
- CI: [GitHub Actions]

Genera:
1. El `stryker.config.json` completo con: mutant: ["arithmetic", "conditional", "boolean"],
   coverageAnalysis: "off", thresholds con un mutation score mínimo realista
2. El fichero de workflow para GitHub Actions que lo ejecute de forma scheduled
   (nightly), NO en cada pull request, y que suba el informe HTML como artefacto
3. Un script en package.json para ejecutarlo solo sobre una carpeta concreta
   (mis módulos de negocio), no sobre todo el repo
4. Explicación de qué hacer con los "mutantes supervivientes" que están en código de test,
   código generado o código de terceros — no se pueden matar con tests

Un umbral de 80% de mutation score en un proyecto existente va a fallar el primer día.
Dime qué valor de arranque realista usar, y cómo subirlo de forma incremental por módulos.

Ejemplo real:

{
  "$schema": "./node_modules/stryker/schema/stryker-schema.json",
  "packageManager": "npm",
  "testRunner": "vitest",
  "mutate": ["src/domain/**/*.ts"],
  "reporters": ["html", "clear-text", "progress"],
  "coverageAnalysis": "off",
  "thresholds": { "high": 80, "low": 60, "break": 55 },
  "mutant": ["arithmetic", "conditional", "boolean", "string"]
}

La clave está en mutate: apuntar solo a src/domain/ en lugar de todo el repo. Es lo que hace esto viable en un proyecto grande.

3. Characterisation testing para código legacy

El patrón de Michael Feathers es el que más subestimado. Cuando heredas una función de 400 líneas de la que nadie entiende, no escribas tests de lo que debería hacer. Escribe tests de lo que hace, documentando el comportamiento (bugs incluidos) para congelarlo antes de tocarlo.

Tengo esta función heredada de [año aproximado], sin tests, en un proyecto en producción.
Nadie sabe exactamente qué hace en todos los casos.

Función:
[pega la función]

Quiero aplicar characterisation testing (el patrón de Michael Feathers): congelar
el comportamiento ACTUAL para poder refactorizar con red de seguridad.

Genera:
1. Una lista de los casos de comportamiento que esta función tiene, inferidos del código (no lo que
   debería hacer, lo que hace)
2. Para cada caso: un test que lo verifique, marcando con un comentario `// ⚠️ BUG
   CONOCIDO` los que creo que son bugs heredados
3. Los casos que NO puedo verificar leyendo el código, porque dependen de datos externos,
   y cómo obtener esos datos para poder testearlos
4. Una lista de "preguntas abiertas": lo que necesito preguntar al equipo o al cliente
   para saber si ese comportamiento raro es intencional

Nada de tests que fallen. Los tests de caracterización tienen que pasar contra el
código actual, incluso si ese código está mal. El objetivo es la red, no la corrección.

Por qué funciona: el modelo razona sobre el flujo de control real y encuentra los caminos que devuelven valores raros. Esas ramas normalmente no están cubiertas y son donde viven los bugs más caros.

Ejemplo real:

describe('calcularDescuento (caracterización del legacy)', () => {
  it('aplica el 10% por encima de 100€', () => {
    expect(calcularDescuento({ total: 200 })).toBe(180);
  });

  it('⚠️ BUG CONOCIDO: devuelve NaN con total null', () => {
    expect(calcularDescuento({ total: null })).toBeNaN();
  });

  it('no aplica descuento en el límite exacto de 100€', () => {
    expect(calcularDescuento({ total: 100 })).toBe(100);
  });

  it('⚠️ comportamiento raro: aplica dos veces si hay cupón y descuento automático', () => {
    expect(calcularDescuento({ total: 200, cupon: 'VERANO' })).toBe(144);
  });

  it('redondea a 2 decimales pero solo en un lado', () => {
    expect(calcularDescuento({ total: 133.33 })).toBe(119.997);   // 6 decimales
  });
});

Con eso tienes una red que aguanta el refactor y tres tickets de bug abiertos de golpe.

4. Golden master / approval testing para salidas difíciles de verificar

Golden master (también llamado approval test o snapshot del dominio) es perfecto cuando el output es demasiado grande o aleatorio para un assert normal: un JSON de configuración, un informe, un export.

Quiero aplicar el patrón Golden Master a esta función:

Función:
[pega la función que genera el output]

Problema: el output tiene [X] elementos y [Y] campos, y su forma cambia según la versión
de la librería [nombre]. No puedo escribir 40 asserts.

Quiero:
1. Un generador de fixtures: inputs representativos que cubran los casos interesting
   (vacío, mínimo, típico, máximo, con caracteres especiales, con datos de otra versión)
2. El harness de approval testing que guarda el golden master en disco la primera vez
   y compara en las siguientes
3. La forma de DIFF cuando falla, legible por una persona, no un volcado de 400 líneas
4. El comando exacto para regenerar los goldens cuando el cambio es intencional,
   y para regenerar solo UNO de ellos sin tocar los demás
5. Qué NO debería ir al golden master (datos aleatorios, timestamps, IDs autogenerados:
   dímelo y dame la técnica para normalizarlos antes de comparar)

Ejemplo real con Vitest y Vitest-approve:

import { describe, it, expect } from 'vitest';
import { calculateReport } from './report.js';
import { test } from 'vitest-approve';

const fixtures = [
  { name: 'carrito vacío', input: { items: [] } },
  { name: 'pedido típico', input: { items: [item(1), item(2)] } },
  { name: 'con cupón', input: { items: [item(1)], coupon: 'VERANO' } },
  { name: 'cantidad máxima', input: { items: Array(500).fill(item(1)) } },
  { name: 'unicode', input: { items: [item(1, '日本語 — artículo")] } },
];

describe('calculateReport', () => {
  for (const f of fixtures) {
    test(f.name, () => {
      const result = normalize(calculateReport(f.input));
      expect(result).toMatchApproved();
    });
  }
});

function normalize(report) {
  return JSON.stringify(report, (key, value) => {
    // Los valores no deterministas NO van al golden master
    if (key === 'generatedAt') return '<TIMESTAMP>';
    if (key === 'requestId') return '<UUID>';
    return value;
  }, 2);
}

El normalize con el replacer de JSON.stringify es la parte que casi nadie incluye y la que evita el típico “el test falla cada vez porque cambia el timestamp”.

5. Snapshot testing bien hecho: cuándo actualizar y cuándo no

La regla de oro: el snapshot se lee en el diff. Si nadie lee el diff, no es un test, es un fichero que se regenera a base de -u. Esta es la pregunta que separa los dos casos.

Tengo este componente con snapshots que se actualizan sin revisar y creo que ya no
detectan nada:

Componente y tests:
[pega el componente y los tests con toMatchSnapshot]

Audítame:
1. ¿Cuántos de estos snapshots podrían cambiar sin que nadie note nada? Esos son falsos positivos
2. ¿Cuántos son tan grandes que nadie los va a leer en un diff?
3. ¿Qué partes del snapshot deberían ser asserts explícitos en lugar de snapshot?
4. Cuáles de estos snapshots son legítimamente variables (fechas, IDs aleatorios) y
   necesitan normalización
5. Escribe el política de updates: cuándo se hace `npm test -- -u` y cómo me obligo
   a mirar el diff antes (hook de git, script de verificación, lo que funcione)
6. Convierte los 3 snapshots más importantes en asserts explícitos, uno a uno

Presupuesto: 30 minutos. Quiero cambios aplicables hoy.

Por qué funciona: la distinción entre “snapshot útil” y “snapshot inflado” depende del tamaño del output y de su variabilidad, que el modelo puede estimar leyendo el código. Convertir los grandes en asserts explícitos es lo que devuelve cobertura real.

Ejemplo real:

// ❌ Snapshot de 400 líneas de un objeto entero: nadie lo lee
expect(result).toMatchSnapshot();

// ✅ Snapshot de la forma (que cambia rara vez, y cuando cambia es importante)
expect(Object.keys(result).sort()).toMatchSnapshot();

// ✅ Asserts explícitos para lo que realmente importa
expect(result.total).toBe(180);
expect(result.tax).toBe(0);
expect(result.appliedDiscounts).toHaveLength(1);
expect(result.warnings).toEqual([]);

Y la política, que es lo que evita la degradación:

{
  "scripts": {
    "test:approve": "vitest --update",
    "test": "vitest"
  },
  "lint-staged": {
    "*.{ts,tsx}": [
      "vitest related --run",
      "git diff --name-only --cached | grep -E '\\.(snap|approved)\\.(ts|snap)$' | xargs -r -n1 sh -c 'echo \"⚠️  Snapshot actualizado: $0 — revisa el diff antes de commitear\"'"
    ]
  }
}

6. Regresión visual con Playwright: cuando el diseño es la API

Si tienes un componente de 200 líneas de CSS grid, ningún assert unitario te va a decir que un botón se ha movido 8 píxeles. Eso solo lo detecta una captura visual.

Quiero añadir tests de regresión visual a mi proyecto con Playwright.

Stack:
- App: [Next.js 15 App Router / Astro 5]
- Estilos: [Tailwind 3.4 + componentes shadcn]
- CI: [GitHub Actions en ubuntu]
- Viewports que me interesan: [375x812, 768x1024, 1440x900]

Quiero:
1. La configuración de Playwright con `toHaveScreenshot` con `maxDiffPixelRatio`. Explica
   un valor razonable y por qué no pondría 0
2. Una estrategia de snapshots para evitar que los tests fallen en cada deploy:
   me refiero a fonts, animaciones y elementos con contenido dinámico (fechas, avatares)
3. El código para esperar a que las fuentes estén cargadas y las animaciones terminadas
   ANTES de capturar, o los snapshots salen con la tipografía del sistema
4. Cómo ejecutar esto en CI con el viewport correcto y las fuentes de mi app,
   sin que falle porque la máquina de GitHub Actions tiene otra versión de Chrome
5. El workflow completo que sube los screenshots cuando falla, para poder verlos
6. Una lista de las 3 rutas donde el snapshot visual da más valor que los otros
   tipos de test en MI caso, y por qué esas y no otras

Nada de "usa toHaveScreenshot()". Quiero el código y las decisiones justificadas.

Ejemplo real:

import { test, expect } from '@playwright/test';

// Las fuentes SIEMPRE antes de capturar, o comparas Arial contra Inter
async function esperarEstabilidad(page) {
  await page.evaluate(() => document.fonts.ready);
  await page.evaluate(() =>
    Promise.all(
      document.getAnimations().map((a) => a.finished.catch(() => {})),
    ),
  );
  // Congela el tiempo para que las fechas relativas no cambien entre capturas
  await page.clock.install({ time: new Date('2026-01-15T10:00:00Z') });
}

test.describe('regresión visual — catálogo', () => {
  for (const viewport of [
    { name: 'movil', width: 375, height: 812 },
    { name: 'tablet', width: 768, height: 1024 },
    { name: 'escritorio', width: 1440, height: 900 },
  ]) {
    test(`catálogo — ${viewport.name}`, async ({ page }) => {
      await page.setViewportSize({ width: viewport.width, height: viewport.height });
      await page.goto('/catalogo');
      await esperarEstabilidad(page);

      await expect(page).toHaveScreenshot(`catalogo-${viewport.name}.png`, {
        maxDiffPixelRatio: 0.01,      // 1% de tolerancia: hay antialiasing
        animations: 'disabled',       // k disables CSS animations
        threshold: 0.2,               // tolerancia por pixel en escala 0-255
      });
    });
  }
});

El animations: 'disabled' y el document.fonts.ready son las dos líneas que separan un test de screenshots útil de uno que falla el 40% de las veces.

7. Detectar y matar flaky tests

Un test flaky es peor que no tener ese test: entrena al equipo a re-lanzar el pipeline cuando sale rojo, y ese re-lanzar esconde bugs reales. Aquí el prompt es más de análisis que de generación.

Tengo tests flaky en mi suite. Cada día tengo 3-5 rojos que al re-lanzar pasan.
Esta es la información:

Histórico de fallos (últimos 14 días):
[pega una tabla: test, % de runs fallidos, timestamps de los fallos]

Salida del fallo cuando falla:
[pega el stack trace real]

Analiza:
1. ¿Es realmente flakiness o es un test que falla siempre y alguien está re-lanzando?
   El patrón temporal lo dice: dispersión = flaky, concentración = bug real
2. ¿Cuál de estas 6 categorías es? Tabla de categorías con señales concretas:
   - Orden de ejecución (depende de otro test)
   - Estado compartido (DB, fichero, cache, variable de módulo)
   - Temporización (sleeps, timeouts, esperas de red)
   - Carrera (concurrencia real, no simulada)
   - Aleatoriedad no sembrada (Math.random, dates, UUIDs)
   - Entorno (hora del día, TZ, locale, recursos)
3. Para cada test, el diagnóstico con el dato que lo confirma
4. El fix concreto, en código, no "añade un wait"
5. Cómo evitar que vuelva a pasar: hook, linter, o estructura de test

Por qué funciona: la clave del prompt es el histórico. La diferencia entre “siempre falla” y “falla a veces” no se ve leyendo el stack trace, se ve en la distribución temporal. Esa es la aportación real del prompt.

Ejemplo real de fix para las dos categorías más comunes:

// ❌ Depende del orden: si otro test deja datos, este pasa o falla según el orden
it('finds orders', async () => {
  await db.order.create({ userId: 'u1', total: 100 });
  const orders = await getOrdersForUser('u1');
  expect(orders).toHaveLength(1);   // pasa si u2 no existe, falla si otro test la creó
});

// ✅ Aislamiento: cada test parte de un estado conocido
beforeEach(async () => {
  await db.order.deleteMany({});     // base de datos de test, nunca producción
  await db.user.create({ id: 'u1', email: 'a@test.com' });
});

// ❌ Espera por tiempo
it('processes payment', async () => {
  await processPayment();
  await new Promise((r) => setTimeout(r, 1000));   // y si tarda 1100ms?
  expect(await getStatus()).toBe('done');
});

// ✅ Espera por condición
await expect.poll(() => getStatus(), { timeout: 5000 }).toBe('done');

8. Localizar el commit culpable con git bisect asistido

Cuando ya sabes que el bug existe pero no desde cuándo, git bisect más un test de reproducción es la herramienta. Y aquí la IA es un buen copiloto para diseñar el test de reproducción, que es la parte tediosa.

Existe un bug: [descripción exacta del síntoma]. Sé que empezó en algún momento entre
el commit [hash-inicial] y el commit [hash-actual]. Hay ~400 commits en medio.

No sé cuándo apareció. Tengo [entorno donde reproducirlo]: [URL de staging / comando /
script de Playwright].

Ayúdame con tres cosas:
1. Escribe el test mínimo que reproduce ESTE bug. Debe fallar en el commit actual y
   pasar en el inicial. Criterios: rápido (<10s), determinista, sin datos externos
   que cambien entre commits
2. Escribe el comando de git bisect completo, con `git bisect start`, `git bisect bad`,
   `git bisect good`, y `git bisect run [mi test]`, explicando qué hace cada paso
3. ¿Qué puede hacer fallar el bisect? (dependencias que cambian de versión, migraciones,
   datos, cambios de configuración). Dame cómo evitarlo, por ejemplo `git bisect skip`
   en commits no testeables
4. Una vez encontrado: cómo hacer `git show` del commit sospechoso y explicar el diff
   en detalle, especialmente si toca los ficheros de [directorio donde sospechas que está]

Y una pregunta: ¿hay un modo más rápido que bisect si el bug es visual o de timing?

Por qué funciona: el modelo puede diseñar el test de reproducción si describes el síntoma con precisión — y obligar a que sea determinista y rápido es clave, porque un test de 30 segundos convierte un bisect de 400 commits en una tarde entera.

Ejemplo real:

#!/bin/bash
# bisect-test.sh — sale con 0 si el bug NO está (bien), 1 si está (mal), 125 si no se puede probar
timeout 30 npm test -- tests/regresion-descuento.test.ts
CODE=$?
if [ $CODE -eq 124 ]; then
  echo "SKIP: timeout"
  exit 125
fi
exit $CODE
git bisect start
git bisect bad HEAD              # aquí el bug existe
git bisect good a1b2c3d           # aquí no existía
git bisect run ./bisect-test.sh   # automatizado: ~9 pasos para 400 commits

Con 400 commits, git bisect hace unos 9 rounds. A 3 minutos por round, tienes el commit culpable antes de comer.

9. Diseñar la matriz de tests de regresión desde los requisitos

Un test de regresión no debería salir de mirar el código: debería salir de un requisito. Y cuando el requisito está en un ticket, el modelo puede extraer la matriz.

Tengo estos tickets de Jira con los requisitos que deben seguir funcionando.
Cada uno es una regla de negocio que, si se rompe, genera una queja de cliente.

Tickets:
[todos, con su texto completo]

Haz tres cosas:
1. Convierte cada requisito en un test de regresión concreto: nombre del test, input,
   assert esperado y qué pasa si falla
2. Detecta los requisitos AMBIGUOS o que dejan huecos: "aplica descuento si el cliente
   es VIP" — ¿qué pasa con un cliente VIP con el carrito vacío? Reclamo los huecos
   con preguntas concretas para producto
3. Señala los requisitos que tienen MÁS probabilidad de romperse según cómo está
   escrito el código que implementa cada uno
   [pega los ficheros de implementación de cada requisito]
   Priorízalos por riesgo, no por orden del ticket

Quiero los tests que cubran los 5 requisitos de mayor riesgo primero. Si no caben todos,
   quiero saber cuáles dejé fuera y por qué.

Por qué funciona: el modelo puede leer requisito y implementación y detectar dónde no coinciden. Esa discrepancia es exactamente la regresión silenciosa: el código hace una cosa y el ticket dice otra, y ningún test lo nota porque ambos leen el mismo código.

10. Convertir el informe de cobertura en decisiones priorizadas

La cobertura es el dato que todo el mundo tiene y nadie mira. El prompt que la convierte en una lista de acciones.

Este es el informe de cobertura de mi proyecto:

[pega el resumen de la tabla: file | % stmts | % branches | uncovered lines]

Y este es mi tasa de bugs por módulo del último año:
[módulos con nº de incidencias]

Cruza los dos datos y:
1. ¿Qué módulos tienen baja cobertura Y muchos bugs? Esos son la prioridad absoluta
2. ¿Qué módulos tienen alta cobertura y muchos bugs? Eso apunta a otra causa
   (no es falta de tests, es lógica ambigua o specs que cambian)
3. ¿Qué módulos tienen baja cobertura y pocos bugs? No los toques: o no importan,
   o los bugs que dan son triviales
4. Ordena TODOS los ficheros por "riesgo real" = impacto conocido × probabilidad de fallo.
   No por porcentaje de cobertura
5. Para los 3 primeros, dime qué tipo de test falta: unit, integración, de regresión,
   visual o de carga

Y dame un plan a 4 semanas: 1 fichero por semana, empezando por el de mayor riesgo.
Nada de "mejorar la cobertura general". Dime qué fichero y qué test concreto.

Por qué funciona: cruza cobertura con impacto real. Los dos datos por separado no dicen nada; juntos te dan la lista de cinco ficheros que contienen el 80% del riesgo.

Catálogo de herramientas de detección

TécnicaDetectaCostoCuándo
Tests unitariosLógica incorrecta obviaBajoSiempre
Mutation testingTests que no verifican nadaAltoLógica de negocio crítica
Snapshot testingCambios de output no intencionadosMedioComponentes con output estable
Golden masterCambios en estructuras grandesMedioExportaciones, configuraciones
CharacterisationCambios en legacy sin cubrirBajoAntes de refactorizar legacy
Regresión visualCambios de maquetaciónMedio-altoInterfaces críticas
git bisect + testEl commit culpableBajoCuando ya sabes que hay bug
Contract testingCambios en la API entre equiposMedioMicroservicios, front y back separados
Load testingCuellos de botella por tráficoMedioAntes de campañas o features grandes
Test de contrato con OpenAPIQue el cliente espera lo que el server daBajoCuando alguien cambia la API

Anti-patrones

1. Confundir cobertura con calidad de tests

❌ "Subimos la cobertura del 62% al 87%, estamos bien"
   → 87% de líneas ejecutadas. Mutantes pueden sobrevivir en todas ellas.
✅ Stryker sobre src/domain/ → mutation score del 76%, y los 4 mutantes
   supervivientes nos dicen exactamente qué asserts faltan.

2. El snapshot que nadie lee

❌ git commit -a   # con los snapshots cambiados, sin mirar el diff
   → 3.000 líneas de snapshot regeneradas y ni un ojo humano las ha visto
✅ Hook de lint-staged que avisa, y la regla: si el diff del snapshot es más de
   50 líneas, se revisa en el PR. Nunca `npm test -- -u` en bloque sin contexto.

3. Tests generados por la misma IA que el código

❌ Prompt: "escribe la función y sus tests"  → mismo modelo, mismo error, tests verdes
✅ Prompt 1: TÚ escribes el comportamiento esperado, en un test, en un mensaje
   Prompt 2: la IA implementa hasta que el test pasa
   El test es el contrato, no un reflejo de la implementación.

4. Re-lanzar el pipeline cuando sale rojo

❌ "El rojo era flaky, re-lanza"  — tres veces por semana
✅ Los tests flaky se arreglan antes de añadir más tests. Un 5% de falsos positivos
   en CI significa que el equipo ya no lee los rojos. Y uno de esos rojos "flaky"
   era el bug real.

5. Characterisation test que “mejora” el legacy

❌ Al escribir los tests del legacy, cambiás el comportamiento para que "sea lo correcto"
   → Rompes producción. Nadie pidió ese cambio.
✅ Los tests de caracterización documentan lo que hay, bugs incluidos, marcados.
   El cambio de comportamiento es un PR aparte, con su propio análisis de impacto.

6. Ignorar el git bisect por pereza

❌ "Recuerdo que el bug empezó por un PR de diciembre..."  — 3 meses de commits
✅ git bisect run con un test de reproducción: 9 rounds, 30 minutos, el commit exacto

Errores frecuentes

“Stryker se queda sin memoria y revienta el build.” El threshold alto obliga a cargar muchos módulos. Sube max-old-space-size a 4GB y limita con mutate: ["src/critico/**"] en lugar de todo el repo.

“Los snapshots fallan solo en CI, no en local.” Casi siempre por las fuentes o por la versión de Chrome en la imagen de Ubuntu. Fijá la versión del navegador con npx playwright install chromium --with-deps y una versión exacta en package.json. Los screenshots tienen que generarse siempre en la misma plataforma.

“El test de caracterización falla después de un refactor.” Significa que no era caracterización sino que ya asumías el comportamiento. Si el golden master falla, el refactor cambió el comportamiento. Para eso es la red: para que te avise antes del deploy, no después.

“git bisect run se salta la mitad de los commits.” Suele ser que el test necesita dependencias que cambian, o migraciones de base de datos. Usa git bisect skip a mano para esos casos y vuelve a lanzar el bisect con la lista de buenos ya marcada. El commit se sostiene igual.

“El mutation score es del 40% y no sé por dónde empezar.” Ejecuta con --reporters html y mira la lista de mutantes supervivientes ordenada por “mutante que cambia más comportamiento” (ConditionalExpression suele ir primero). Empieza por ahí, sube el score de golpe, no de golpe. Mejor: subelo en pasos del 5%.

El flujo completo

┌──────────────────────────────────────────────────────────────┐
│  0. ¿TENEMOS UN BUG O UN RIESGO?                             │
│     Bug conocido → paso 1. Riesgo latente → paso 2          │
└──────────────┬───────────────────────────────┬───────────────┘
               ▼                               ▼
┌──────────────────────────────┐  ┌──────────────────────────┐
│  1. TEST DE REPRODUCCIÓN     │  │  2. MUTATION TESTING      │
│  mínimo, determinista, <10s  │  │  Stryker sobre src/domain │
│  → git bisect run            │  │  → mutantes supervivientes│
│  → commit culpable            │  │  → asserts que faltan    │
└──────────────────────────────┘  └──────────────────────────┘
               │                               │
               └───────────────┬───────────────┘
                               ▼
┌──────────────────────────────────────────────────────────────┐
│  3. ¿CÓDIGO NUEVO O LEGACY?                                  │
│     Nuevo → tests de requisitos antes de implementar          │
│     Legacy → characterisation tests (congelar el actual)      │
└──────────────────────────────┬───────────────────────────────┘
                               ▼
┌──────────────────────────────────────────────────────────────┐
│  4. ELEGIR LA TÉCNICA ADECUADA                               │
│     Lógica → unit + mutation                                 │
│     Output grande → golden master                             │
│     Maquetación → Playwright screenshot                       │
│     Requisito vs código → contract test                       │
└──────────────────────────────┬───────────────────────────────┘
                               ▼
┌──────────────────────────────────────────────────────────────┐
│  5. EL SNAPSHOT SE LEE, NO SE REGENERA                       │
│     diff > 50 líneas → revisión obligatoria en el PR         │
│     Assertion explícita > snapshot para lo que importa       │
└──────────────────────────────┬───────────────────────────────┘
                               ▼
┌──────────────────────────────────────────────────────────────┐
│  6. FLKY TESTS A CERO ANTES DE AÑADIR MÁS                    │
│     CI con falsos positivos = CI que nadie escucha            │
└──────────────────────────────┬───────────────────────────────┘
                               ▼
┌──────────────────────────────────────────────────────────────┐
│  7. PRIORIZAR POR RIESGO REAL, NO POR COBERTURA              │
│     cobertura × impacto conocido = la cola de trabajo        │
└──────────────────────────────────────────────────────────────┘

Conclusión

La regresión silenciosa no se arregla escribiendo más tests. Se arregla haciendo que los tests affirmen algo que pueda fallar, y esa es exactamente la tarea que delegas en IA cuando le pides que enumere mutantes en lugar de que te escriba asserts bono.

Tres reglas que me llevo. Primera: nunca la misma IA para la implementación y sus tests, porque comparten los mismos supuestos. Segunda: characterisation antes que refactor en legacy, porque documentar lo que hay es más barato que adivinar lo que debería haber. Tercera: un snapshot sin diff revisado es un fichero, no un test.

Para lo que viene después del deploy, dos artículos que encajan aquí. Si lo que te preocupa es que el servidor aguante el tráfico, está prompts para load testing con k6. Y si lo que temes es que la página cargue lenta con los usuarios reales, prompts para analizar rendimiento web.

La pieza que falta en todo esto es escribir bien los prompts. Empieza por los mejores prompts para programar con IA, y si ya tienes claro el framework que quieres, la herramienta interactiva te ahorra el trabajo repetitivo. Para el contexto largo —y los prompts de este artículo son largos—, mira cómo aprovechar la ventana de contexto. Y si el problema es que la IA genera código que no sabes ni depurar, vuelve a testear código generado por IA.

Fran Cobos

Fran Cobos

Desarrollador Full Stack especializado en IA aplicada, automatización y desarrollo web. Escribo sobre herramientas, tutoriales y casos reales para programadores.

¿Necesitas desarrollo a medida?

Apps web, IA, módulos ERP — cuéntame tu proyecto.