12k
All articles

¿Sigue importando el TDD cuando los agentes escriben el código?

Vea cómo el desarrollo guiado por pruebas controla a los agentes de IA: escriba primero pruebas Vitest, evite que las alteren y verifique el código en CI.

OpenReplay Team
OpenReplay Team
¿Sigue importando el TDD cuando los agentes escriben el código?

Sí, y posiblemente más que antes, pero como contrato y no como ritual: cuando un agente de programación escribe la implementación, los tests escritos primero se convierten en la especificación. Definen qué significa «terminado» antes de que exista una sola línea de código y delatan al agente cuando da por completada una tarea que no lo está.

La mayoría de los desarrolladores que delegan la implementación en Claude Code, Cursor o GitHub Copilot lo han visto. El agente anuncia que todo pasa, y luego la funcionalidad falla al primer uso, o un archivo de tests ha sido reescrito sin avisar para que coincida con el nuevo código.

Este artículo cubre el flujo de trabajo test-first con agentes: un archivo de Vitest real, la instrucción exacta que conviene acompañarle, las formas en que los agentes lo esquivan y las salvaguardas que cierran esos huecos.

Puntos clave

  • Los tests escritos antes de que el agente empiece actúan como una especificación ejecutable: el trabajo del agente es hacer que pasen, no decidir qué significa pasar.
  • Un test escrito después del código tiende a describir lo que el código ya hace, así que pasa por construcción y no comprueba nada.
  • Un mensaje del agente diciendo que todos los tests pasan es una afirmación. Una ejecución de CI sobre el commit subido es la prueba.
  • Si un mismo diff modifica tanto la implementación como las aserciones que la evalúan, el agente se ha puesto la nota a sí mismo; por eso, revisa los cambios en los tests por separado de los cambios en el código.
  • Prescinde del test-first en prototipos desechables y en ajustes puramente visuales, donde no puedes escribir el resultado esperado hasta que lo ves.

¿Qué es red-green-refactor?

Red-green-refactor (rojo-verde-refactorizar) es el ciclo del TDD: escribes un test que falla y que describe un comportamiento concreto (rojo), y después escribes el código mínimo que lo hace pasar (verde). Con la suite en verde, limpias el código sin cambiar lo que hace (refactorizar). Para un repaso más extenso con ejemplos en React, consulta TDD vs BDD in React.

¿Cómo funciona el flujo de TDD con IA?

El TDD con IA funciona cuando una persona escribe o aprueba los tests que fallan y se le indica al agente que los haga pasar sin modificarlos. Los tests contienen los requisitos. El agente solo aporta la implementación. Los ejemplos siguientes usan Vitest 5.

Empieza con un stub, para que el archivo de tests importe algo real:

// src/cart/applyCoupon.ts
export type Coupon =
  | { code: string; kind: 'percent'; value: number }
  | { code: string; kind: 'fixed'; valueCents: number };

export function applyCoupon(
  subtotalCents: number,
  code: string,
  coupons: Coupon[],
): number {
  throw new Error('not implemented');
}

Después escribe la especificación:

// src/cart/applyCoupon.test.ts
import { describe, it, expect } from 'vitest';
import { applyCoupon, type Coupon } from './applyCoupon';

const coupons: Coupon[] = [
  { code: 'SAVE10', kind: 'percent', value: 10 },
  { code: 'FIVEOFF', kind: 'fixed', valueCents: 500 },
];

describe('applyCoupon', () => {
  it('takes a percentage off the subtotal', () => {
    expect(applyCoupon(2000, 'SAVE10', coupons)).toBe(1800);
  });

  it('takes a fixed amount off the subtotal', () => {
    expect(applyCoupon(2000, 'FIVEOFF', coupons)).toBe(1500);
  });

  it('never returns a negative total', () => {
    expect(applyCoupon(300, 'FIVEOFF', coupons)).toBe(0);
  });

  it('matches codes case-insensitively', () => {
    expect(applyCoupon(2000, 'save10', coupons)).toBe(1800);
  });

  it('rejects unknown codes', () => {
    expect(() => applyCoupon(2000, 'BOGUS', coupons)).toThrow('Unknown coupon');
  });
});

Como el stub existe y lanza una excepción, los cinco tests fallan en tiempo de ejecución y no por una importación inexistente. Es una fase roja limpia. El último test merece una mirada más atenta: el stub sí lanza una excepción, pero su mensaje no coincide con 'Unknown coupon', así que sigue fallando por el motivo correcto. Cada test fija una regla (descuento porcentual frente a descuento fijo, el mínimo en cero, el tratamiento de mayúsculas y minúsculas) que, de otro modo, el agente tendría que adivinar.

Después, entrégaselo con restricciones explícitas:

Implement applyCoupon in src/cart/applyCoupon.ts so every test in
src/cart/applyCoupon.test.ts passes.

Rules:
- Do not create, edit, rename, skip or delete any *.test.ts file.
- Do not add .skip, .only, or mocks of applyCoupon.
- If a test looks wrong, stop and explain why instead of changing it.
- When finished, run `npx vitest run src/cart` and paste the full output.

La línea de «detente y explica» le da al agente una salida legítima distinta de modificar el test. La salida pegada te ahorra un paso, pero no demuestra nada. Encontrarás más hábitos de prompting que encajan con este ciclo en 5 Tips and Tricks for AI-Assisted Coding.

¿Cómo falla el test-first con agentes?

Los agentes tienden a buscar el camino más corto hacia una ejecución en verde, y hay tres caminos que se saltan la especificación por completo.

Editar o eliminar tests. Cuando una implementación es difícil, cambiar la expectativa resulta más fácil que cumplirla. Un toBe(1800) modificado, un .skip nuevo o un archivo de tests que ha desaparecido producen una ejecución en verde en la que el requisito se ha eliminado.

Aserciones débiles. Un test escrito después del código tiende a confirmar lo que el código hace, no lo que debería hacer:

// Confirms the code ran, not that it's right
it('applies coupon', () => {
  const result = applyCoupon(2000, 'SAVE10', coupons);
  expect(result).toBeDefined();
  expect(typeof result).toBe('number');
});

// Pins the behaviour
it('takes 10% off with SAVE10', () => {
  expect(applyCoupon(2000, 'SAVE10', coupons)).toBe(1800);
});

El test débil con toBeDefined() pasa tanto si applyCoupon devuelve 0 como 2000 o 999999. Por eso importa el orden en que se escriben: los tests que el agente escribe después de su propio código tienden a parecerse a la primera versión. Además, los agentes suelen rellenar las suites con muchas comprobaciones superficiales como esta, un patrón que se analiza en los smoke tests que los agentes no dejan de escribir.

Mockear lo que se está probando. Mockear el módulo bajo prueba significa que el test ejercita el mock, y ese test seguirá en verde haga lo que haga la implementación real:

import { vi } from 'vitest';

vi.mock('./applyCoupon', () => ({ applyCoupon: () => 1800 }));
// Every assertion now tests the mock.

Vitest eleva (hoisting) vi.mock por encima de los imports, de modo que la factory sustituye el módulo antes de que se cargue el código real. El applyCoupon real nunca se ejecuta. Mockea las fronteras (red, almacenamiento, tiempo), no la unidad que estás especificando.

¿Qué salvaguardas mantienen fija la especificación?

Las salvaguardas funcionan cuando son mecánicas. Las instrucciones reducen la manipulación de los tests, y la CI y la revisión de código detectan lo que las instrucciones no logran evitar.

Revisa los diffs de los tests por separado. Lee los cambios en *.test.ts como una revisión independiente, antes que la implementación. En GitHub, CODEOWNERS con revisión obligatoria por parte del code owner puede garantizar que una persona concreta apruebe cualquier cambio en las rutas de tests.

Bloquea o prohíbe al agente editar los tests.

Deja que la CI decida qué está «terminado». Ejecuta npx vitest run sobre el commit subido. En CI, Vitest hace fallar la ejecución si encuentra .only, salvo que pases --allowOnly o configures allowOnly: true. No detecta .skip, así que añade una comprobación del diff en las ramas de implementación:

changed=$(git diff --name-only origin/main...HEAD -- '*.test.ts' '*.spec.ts')
if [ -n "$changed" ]; then
  echo "Test files changed on an implementation branch:"
  echo "$changed"
  exit 1
fi

El checkout necesita historial suficiente para que origin/main pueda resolverse.

Comprueba que las aserciones prueban comportamiento. Para cada aserción, pregúntate si una implementación incorrecta podría pasarla igualmente. Los valores exactos, las excepciones lanzadas y la salida visible sobreviven a las refactorizaciones. Los conteos de llamadas a helpers internos y las comprobaciones de tipo, no.

Incluso una suite impecable tiene un límite. Que los tests unitarios pasen demuestra que el código maneja los casos que alguien dejó por escrito. No demuestra que un usuario real pueda completar el flujo en un navegador, y los session replays de una funcionalidad ya publicada pueden mostrar el proceso de pago fallando mientras la CI está en verde.

¿Cuándo no compensa el TDD?

Escribir los tests primero cuesta más de lo que aporta cuando no puedes enunciar el resultado esperado de antemano.

SituaciónPor qué prescindir del test-first
Prototipos desechablesEl código sirve para responder una pregunta y después se elimina
Ajustes visuales de UI (espaciado, color, maquetación)El resultado correcto se juzga mirándolo, así que todavía no hay nada que afirmar

Si un prototipo sobrevive, escribe los tests antes de que el agente lo consolide como código de producción.

Conclusión

Cuando los agentes escriben el código, la especificación es la parte que solo tú puedes aportar, y los tests escritos primero la hacen ejecutable. En tu próxima tarea con un agente, escribe primero el archivo de Vitest que falla, dale al agente la instrucción de no editarlo y haz merge solo cuando tanto la CI como una revisión independiente del diff de los tests den el visto bueno.

Preguntas frecuentes

¿Puede un linter detectar los tests omitidos que añade un agente?

Sí. El paquete @vitest/eslint-plugin incluye la regla vitest/no-disabled-tests, que marca test.skip, it.skip y describe.skip, y la regla vitest/no-focused-tests, que marca .only. La configuración recomendada solo emite advertencias para los tests deshabilitados, así que establece esa regla en 'error' y ejecuta ESLint en CI. Los proyectos con Jest disponen de reglas con los mismos nombres en eslint-plugin-jest. El linter puede pasar por alto llamadas con alias, así que mantén también la comprobación del diff con git.

¿Puede un agente de IA escribir los tests que fallan en lugar de una persona?

Sí, siempre que escribir los tests y escribir la implementación sean pasos separados. Haz que una sesión del agente redacte los tests contra el stub, revisa tú mismo las aserciones y haz commit de ellas antes de que empiece la implementación. La sesión que implementa solo debe ver tests que no tiene permitido modificar. Si la misma sesión escribe ambas cosas, los tests acaban describiendo su propio código en lugar de los requisitos.

¿Basta con una especificación escrita detallada sin escribir los tests primero?

No. Una especificación en prosa o un stub con comentarios de documentación le dice al agente qué construir, pero nada contrasta el resultado con ella. Un test que falla no se satisface con una implementación que simplemente parece plausible: o pasa o no pasa. Usa ambas cosas. Pon los requisitos y las restricciones en el prompt o en el archivo de instrucciones, y codifica como tests los casos que deben cumplirse para que la CI los ejecute en cada commit.

¿Qué debo hacer cuando el agente dice que un test está mal?

Revisa tú mismo la explicación del agente y, si el test realmente está mal, corrígelo fuera de la rama de implementación. Integra el test corregido en main en su propio commit o pull request, revisado mediante CODEOWNERS si lo utilizas, y después haz rebase de la rama de implementación. La comprobación del diff compara la rama con origin/main, así que una corrección de un test que ya está en main no la activa, y el agente nunca edita la especificación.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.