Saltar al contenido principal

Cómo Testear Código Generado por IA: Tests Automáticos para Copilot, Cursor y Claude

El código de Copilot y Cursor compila pero ¿funciona? Framework de testing para validar código generado por IA: unit tests, snapshot testing, property-based testing y CI.

Fran Cobos 9 min de lectura 1642 palabras

Tabla de contenidos

Copilot te genera una función en 5 segundos. Compila. El tipo de retorno es correcto. La integras en tu código y haces deploy.

Una semana después: un bug en producción. La función no manejaba arrays vacíos. O convertía "0" a false. O mutaba el argumento de entrada.

El problema no es que la IA genere “mal código” — es que genera código que parece correcto pero no cubre los edge cases de tu dominio. La solución: tests automatizados diseñados específicamente para validar código de IA.

Por qué el código de IA necesita tests especiales

Los LLMs tienen patrones de error predecibles:

Tipo de errorEjemploFrecuencia
Edge cases ignoradosNo maneja null, undefined, arrays vacíosMuy común
Off-by-one<= en vez de <, índices incorrectosComún
Mutación de inputsModifica el array/objeto originalComún
Tipos implícitosAsume string cuando puede ser numberOcasional
Lógica de negocioImplementa una aproximación, no la regla exactaFrecuente
SeguridadSQL injection, XSS, path traversalOcasional

Un test suite convencional podría no detectar estos errores porque suele testar el “camino feliz”. Necesitas una estrategia específica.

Estrategia 1: Test-First (la más robusta)

Escribe los tests antes de pedirle el código a la IA. Así:

  1. Defines exactamente el comportamiento esperado
  2. La IA genera la implementación
  3. Corres los tests → pasan o no
// 1. TÚ escribes el test primero
import { describe, it, expect } from 'vitest';
import { calculateDiscount } from './pricing.js';

describe('calculateDiscount', () => {
  // Happy path
  it('aplica 10% para compras > 100€', () => {
    expect(calculateDiscount(150)).toBe(135);
  });

  // Edge cases que la IA probablemente ignorará
  it('no aplica descuento para compras <= 100€', () => {
    expect(calculateDiscount(100)).toBe(100);
    expect(calculateDiscount(50)).toBe(50);
  });

  it('maneja 0€ correctamente', () => {
    expect(calculateDiscount(0)).toBe(0);
  });

  it('rechaza valores negativos', () => {
    expect(() => calculateDiscount(-10)).toThrow();
  });

  it('redondea a 2 decimales', () => {
    expect(calculateDiscount(101)).toBe(90.90);
  });

  it('maneja valores no numéricos', () => {
    expect(() => calculateDiscount('abc')).toThrow();
    expect(() => calculateDiscount(null)).toThrow();
  });
});

// 2. Ahora le pides a Copilot/Cursor que implemente calculateDiscount
// 3. Corres los tests: vitest run

Tip: Puedes pedirle a la IA que genere el esqueleto de tests, pero siempre revisa y añade edge cases manualmente. La IA tiende a generar tests que “pasan” su propia implementación — no los que la desafían.

Estrategia 2: Snapshot Testing para output complejo

Cuando la IA genera funciones que producen output complejo (HTML, JSON, configuraciones), usa snapshot testing para detectar regresiones:

import { describe, it, expect } from 'vitest';
import { generateEmailTemplate } from './email.js';

describe('generateEmailTemplate', () => {
  it('genera template de bienvenida correctamente', () => {
    const html = generateEmailTemplate({
      type: 'welcome',
      userName: 'Fran',
      planName: 'Pro'
    });

    // La primera vez crea el snapshot, las siguientes compara
    expect(html).toMatchSnapshot();
  });

  it('escapa caracteres HTML en el nombre de usuario', () => {
    const html = generateEmailTemplate({
      type: 'welcome',
      userName: '<script>alert("xss")</script>',
      planName: 'Pro'
    });

    expect(html).not.toContain('<script>');
    expect(html).toContain('&lt;script&gt;');
  });
});

Estrategia 3: Property-Based Testing

En vez de testear casos específicos, defines propiedades que siempre deben cumplirse. fast-check genera cientos de inputs aleatorios:

import { describe, it } from 'vitest';
import fc from 'fast-check';
import { sortProducts } from './catalog.js';

describe('sortProducts (generado por IA)', () => {
  it('siempre devuelve la misma cantidad de elementos', () => {
    fc.assert(
      fc.property(
        fc.array(fc.record({
          name: fc.string(),
          price: fc.float({ min: 0, max: 10000 }),
        })),
        (products) => {
          const sorted = sortProducts(products, 'price');
          return sorted.length === products.length;
        }
      )
    );
  });

  it('el resultado está ordenado por precio', () => {
    fc.assert(
      fc.property(
        fc.array(fc.record({
          name: fc.string(),
          price: fc.float({ min: 0, max: 10000, noNaN: true }),
        }), { minLength: 2 }),
        (products) => {
          const sorted = sortProducts(products, 'price');
          for (let i = 1; i < sorted.length; i++) {
            if (sorted[i].price < sorted[i - 1].price) return false;
          }
          return true;
        }
      )
    );
  });

  it('no muta el array original', () => {
    fc.assert(
      fc.property(
        fc.array(fc.record({
          name: fc.string(),
          price: fc.float({ min: 0, max: 10000 }),
        })),
        (products) => {
          const original = JSON.parse(JSON.stringify(products));
          sortProducts(products, 'price');
          return JSON.stringify(products) === JSON.stringify(original);
        }
      )
    );
  });
});

Property-based testing es especialmente potente contra código de IA porque genera inputs que un humano no pensaría: strings vacíos, arrays de 1000 elementos, números NaN, Infinity, caracteres unicode raros.

Estrategia 4: Checklist de seguridad

Los editores de IA como Cursor, Copilot y Windsurf generan código funcional, pero rara vez aplican las mejores prácticas de seguridad por defecto:

import { describe, it, expect } from 'vitest';
import { searchUsers } from './users.js';

describe('seguridad: searchUsers (generado por IA)', () => {
  it('no es vulnerable a SQL injection', async () => {
    // La IA podría haber generado: `SELECT * FROM users WHERE name = '${query}'`
    const maliciousInput = "'; DROP TABLE users; --";
    // Si no tira error y no borra la tabla, está bien
    await expect(searchUsers(maliciousInput)).resolves.toBeDefined();
  });

  it('no expone campos sensibles', async () => {
    const users = await searchUsers('test');
    for (const user of users) {
      expect(user).not.toHaveProperty('password');
      expect(user).not.toHaveProperty('password_hash');
      expect(user).not.toHaveProperty('token');
    }
  });

  it('limita la cantidad de resultados', async () => {
    const users = await searchUsers('a'); // query muy genérica
    expect(users.length).toBeLessThanOrEqual(100);
  });
});

El flujo completo: IA + tests en tu workflow

┌─────────────────────────────────────────────┐
│  1. Escribe tests (TÚ)                     │
│     └→ Define comportamiento esperado       │
│                                             │
│  2. Genera código (IA)                      │
│     └→ Copilot / Cursor / Claude            │
│                                             │
│  3. Corre tests                             │
│     ├→ ✅ Pasan → Review manual + merge     │
│     └→ ❌ Fallan → Vuelve a paso 2          │
│                                             │
│  4. CI automático                           │
│     └→ Tests en cada PR / commit            │
└─────────────────────────────────────────────┘

Configurar CI para código de IA

# .github/workflows/test.yml
name: Test AI-generated code
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npx vitest run --coverage
      - name: Check coverage threshold
        run: |
          npx vitest run --coverage --coverage.thresholds.lines=80

Regla: el código generado por IA debe tener al menos 80% de cobertura. Si la IA genera código que es difícil de testear, es código que probablemente deberías reescribir.

Anti-patrones: lo que NO hacer

1. Pedirle a la IA que genere los tests Y la implementación

// ❌ Los tests de la IA validan la implementación de la IA
// → Si ambos tienen el mismo error, los tests pasan

La IA tiende a generar tests que “coinciden” con su propia implementación. Son tests tautológicos que no validan nada.

2. Solo testear el happy path

// ❌ Esto es lo que Copilot genera como test
it('suma dos números', () => {
  expect(add(2, 3)).toBe(5);
});

// ✅ Lo que realmente necesitas
it('suma dos números', () => { expect(add(2, 3)).toBe(5); });
it('maneja negativos', () => { expect(add(-1, 1)).toBe(0); });
it('maneja floats', () => { expect(add(0.1, 0.2)).toBeCloseTo(0.3); });
it('maneja strings numéricos', () => { expect(() => add('2', '3')).toThrow(); });

3. Confiar en que “compila = funciona”

TypeScript atrapa errores de tipo, pero no errores de lógica. El código puede tipar perfecto y tener bugs sutiles. Para evitar los errores más comunes al usar IA en tu editor, revisa los errores comunes al programar con Copilot y Cline.

Prompt para generar tests útiles con IA

Si usas IA para ayudarte a generar tests (no como única fuente), este prompt produce buenos resultados:

Genera tests con Vitest para esta función:
[pega la función]

Requisitos:
- Incluye al menos 3 edge cases (null, undefined, array vacío, valores límite)
- Testea que no muta los argumentos de entrada
- Testea tipos incorrectos de input
- Incluye un test de rendimiento para inputs grandes (>10000 elementos)
- NO generes tests que simplemente repitan la lógica de la implementación

Para más prompts efectivos para programar con IA, consulta la guía de mejores prompts para programar con IA.

Herramientas recomendadas

HerramientaPara quéPor qué
VitestUnit + integration testsRápido, compatible con Vite, ESM nativo
fast-checkProperty-based testingGenera inputs que humanos no pensarían
MSWMock de APIsPara testear funciones que llaman a APIs de IA
StrykerMutation testingVerifica que los tests realmente detectan bugs

Testear integraciones con APIs de IA

Si tu código llama a APIs de OpenAI o Claude, no las llames en tests — son lentas, caras e impredecibles. Usa mocks:

import { vi, describe, it, expect } from 'vitest';
import { analyzeText } from './ai-service.js';

// Mock del SDK de OpenAI
vi.mock('openai', () => ({
  default: vi.fn().mockImplementation(() => ({
    chat: {
      completions: {
        create: vi.fn().mockResolvedValue({
          choices: [{ message: { content: '{"sentiment": "positive", "score": 0.9}' } }]
        })
      }
    }
  }))
}));

describe('analyzeText', () => {
  it('parsea correctamente la respuesta de OpenAI', async () => {
    const result = await analyzeText('Me encanta este producto');
    expect(result).toEqual({ sentiment: 'positive', score: 0.9 });
  });
});

Si necesitas que la IA devuelva JSON estructurado de forma fiable (para que tus tests sean predecibles), revisa las técnicas de parseo JSON con IA.

Conclusión

El código generado por IA es como el código de un junior muy rápido: funciona en el happy path pero se rompe en los bordes. Tu trabajo no es revisar cada línea — es escribir tests que la obliguen a ser correcta.

  1. Test-first: escribe tests antes de pedir código
  2. Property-based: genera cientos de inputs aleatorios con fast-check
  3. Seguridad: testea injection, exposición de datos, límites
  4. CI obligatorio: ningún código de IA llega a producción sin tests verdes

La IA acelera el desarrollo 3x. Los tests garantizan que esa velocidad no se traduzca en bugs 3x.

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.