32blogby StudioMitsu

Generación Automatizada de Tests con Claude Code: Guía Práctica

Genera tests de Jest y Vitest automáticamente con Claude Code. Flujo TDD, configuración de cobertura y el bucle que hace que Claude corrija tests fallidos automáticamente.

10 min read
Claude Codetest automationVitestJestTDDTypeScript
Contenido

Le pedí a Claude Code que "escribiera algunos tests" y lo que recibí fue un muro de mocks que no probaban absolutamente nada.

typescript
// What Claude actually gave me
vi.mock('../utils/fetchUser')
vi.mock('../utils/validateInput')
vi.mock('../lib/database')

it('should work', () => {
  expect(mockFetchUser).toHaveBeenCalled()
})

Eso no es un test. Es una verificación de invocación de mock. Después de meses de prueba y error descubriendo cómo lograr que Claude Code genere tests que realmente detecten bugs, aquí está todo lo que he aprendido.

Al final de este artículo, sabrás:

  • La estructura de prompt que hace que Claude genere tests significativos
  • Cómo configurar CLAUDE.md para generación de tests consistente
  • Cómo automatizar un bucle TDD con Vitest
  • El comando que hace que Claude corrija tests fallidos automáticamente
  • Cómo resolver permanentemente el problema del exceso de mocking

¿Qué Hace Diferente la Generación de Tests de Claude Code de las Herramientas de Plantillas?

Cuando GitHub Copilot o herramientas similares generan tests, miran la firma de tu función y rellenan un boilerplate. Es conveniente, pero los tests raramente reflejan lo que el código realmente debería hacer.

La fortaleza de Claude Code es que puede generar tests que entienden la intención de tu código — no solo su interfaz. Pero tienes que provocarlo con las instrucciones correctas.

Sin guía, Claude se enfoca en la interfaz de una función en lugar de su comportamiento. Mockea cada dependencia y verifica que las funciones mockeadas fueron llamadas. Técnicamente no está mal, pero estos tests capturan casi cero bugs.

Antes de generar cualquier test, define lo que significan "buenos tests" para tu proyecto — y comunica esa definición tanto a través de CLAUDE.md como de tus prompts.


¿Cómo Cambian las Cosas al Escribir la Configuración de Tests en CLAUDE.md?

Añadir lo siguiente a CLAUDE.md asegura que Claude Code siempre haga referencia a la filosofía de testing de tu proyecto, independientemente de lo que pidas en cada conversación.

markdown
## Testing Policy

### Tools
- Test runner: Vitest
- Assertions: Vitest built-in (expect)
- Mocking: vi.fn(), vi.spyOn() — minimal use only
- Coverage: @vitest/coverage-v8

### Test Writing Rules
1. Only mock external I/O (DB, HTTP APIs, file system)
2. Do NOT mock utility functions or pure functions
3. Test names follow the format: "when X, it returns Y"
4. Always include edge cases (empty arrays, null, boundary values)
5. Place test files next to the implementation (`*.test.ts`)

### Coverage Thresholds
- statements: 80%
- branches: 75%
- functions: 80%
- lines: 80%

### Prohibited
- Using `as any`
- Testing implementation details (internal variable names, private methods)
- Modifying production code solely to make tests pass

La regla de "solo mockear I/O externo" por sí sola hace una diferencia visible en la calidad de los tests. CLAUDE.md se lee antes de cualquier prompt individual, lo que lo convierte en el lugar más eficiente para establecer convenciones a nivel de proyecto.


¿Por Qué "Escribe Tests" Siempre Falla — Y Cuál Es la Estructura de Prompt Correcta?

Cada prompt de generación de tests debería incluir estos cuatro elementos.

1. Especificar el objetivo

Write Vitest tests for `src/utils/calculateDiscount.ts`.

2. Restringir el alcance

This function has no external dependencies. Do not use mocks.
It's a pure function — test input/output combinations exhaustively.

3. Especificar casos límite

Make sure to cover these cases:
- discountRate is 0
- discountRate is 1.0 (100% off)
- price is 0
- price is negative (expect an error)

4. Definir la convención de nombres

Use the format "when [condition], it returns [result]" for all test names.

Esto es lo que Claude genera con un prompt adecuado:

typescript
import { describe, it, expect } from 'vitest'
import { calculateDiscount } from './calculateDiscount'

describe('calculateDiscount', () => {
  describe('valid inputs', () => {
    it('when discountRate is 0.2, it returns 80% of the price', () => {
      expect(calculateDiscount(1000, 0.2)).toBe(800)
    })

    it('when discountRate is 0, it returns the original price', () => {
      expect(calculateDiscount(1000, 0)).toBe(1000)
    })

    it('when discountRate is 1.0, it returns 0', () => {
      expect(calculateDiscount(1000, 1.0)).toBe(0)
    })
  })

  describe('boundary values', () => {
    it('when price is 0, it returns 0', () => {
      expect(calculateDiscount(0, 0.2)).toBe(0)
    })
  })

  describe('error cases', () => {
    it('when price is negative, it throws an Error', () => {
      expect(() => calculateDiscount(-100, 0.2)).toThrow('Price must be non-negative')
    })

    it('when discountRate exceeds 1, it throws an Error', () => {
      expect(() => calculateDiscount(1000, 1.5)).toThrow('Discount rate must be between 0 and 1')
    })
  })
})

La diferencia con un prompt simple de "escribe tests" es sustancial.


¿Cómo Automatizas un Bucle TDD con Vitest?

Este es el flujo de trabajo TDD que uso con Claude Code.

Paso 1: Genera tests a partir de especificaciones antes de escribir la implementación

Based on the spec below, write Vitest tests. Do NOT write the implementation yet.

Spec:
- Create a `parseJapaneseDate(str: string): Date` function
- It converts strings like "2026年2月26日" into Date objects
- It throws InvalidDateError for malformed input
- Timezone is fixed to JST (UTC+9)

Paso 2: Verifica que los tests fallen

bash
npx vitest run src/utils/parseJapaneseDate.test.ts

Todos deberían fallar — aún no hay implementación. Ese es exactamente el punto de partida que quieres.

Paso 3: Genera la implementación

Write an implementation that makes all tests in
`src/utils/parseJapaneseDate.test.ts` pass.
Do not modify the tests — solve it on the implementation side only.

Paso 4: Itera en modo watch

bash
npx vitest --watch src/utils/parseJapaneseDate.test.ts

Regístralos en package.json para acceso rápido:

json
{
  "scripts": {
    "test": "vitest",
    "test:watch": "vitest --watch",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage"
  }
}

¿Cómo Configuras Umbrales de Cobertura y los Conectas al CI?

Configura la cobertura en vitest.config.ts:

typescript
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      thresholds: {
        statements: 80,
        branches: 75,
        functions: 80,
        lines: 80,
      },
      exclude: [
        'node_modules/**',
        'src/**/*.d.ts',
        'src/**/*.stories.tsx',
        'src/app/**', // Exclude Next.js App Router pages
      ],
    },
  },
})

Añade un workflow de GitHub Actions para forzar la cobertura en cada PR:

yaml
# .github/workflows/test.yml
name: Test

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run test:coverage
      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          files: ./coverage/coverage-final.json

Cuando la cobertura baja del umbral, el job de CI falla y bloquea el merge del PR. Es la forma más efectiva de construir una cultura de testing en un equipo.


¿Cómo Haces que Claude Corrija Tests Fallidos Automáticamente?

Cuando los tests fallan, envía la salida directamente a Claude Code y haz que corrija la implementación.

bash
# Capture test output
npx vitest run 2>&1 | tee /tmp/test-output.txt

Luego pásalo a Claude Code:

Here's the test failure log. Please fix the implementation code.
Do NOT modify any test files.

---
[paste /tmp/test-output.txt contents here]
---

File to fix: src/utils/parseJapaneseDate.ts

Si estás usando la CLI de Claude Code, puedes redirigir esto directamente:

json
{
  "scripts": {
    "test:fix": "vitest run 2>&1 | claude 'Tests are failing. Fix only the implementation files to make them pass. Do not touch test files.'"
  }
}

Este pipeline pasa el log de fallos como contexto automáticamente — no se necesita copiar y pegar manualmente.


¿Cómo Diseñas Prompts para Tests E2E con Playwright?

Los tests E2E son más difíciles de generar que los tests unitarios, pero Claude Code los maneja bien cuando describes los flujos de usuario en lenguaje natural.

Write a Playwright test for the following user story:

User story:
- User navigates to the login page
- User fills in email and password, clicks Submit
- User is redirected to the dashboard
- Page title reads "Dashboard"

Constraints:
- Base URL: http://localhost:3000
- Test user: test@example.com / password123
- Use data-testid selectors only (no class names or id attributes)

Resultado generado:

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

test.describe('Login feature', () => {
  test('redirects to dashboard with valid credentials', async ({ page }) => {
    await page.goto('/login')

    await page.getByTestId('email-input').fill('test@example.com')
    await page.getByTestId('password-input').fill('password123')
    await page.getByTestId('submit-button').click()

    await expect(page).toHaveURL('/dashboard')
    await expect(page).toHaveTitle('Dashboard')
  })
})

Especificar data-testid es clave. Los nombres de clase y atributos id se rompen cuando refactorizas la UI — los atributos de datos específicos para tests se mantienen estables.


¿Por Qué Ocurre el Exceso de Mocking y Cómo lo Solucionas?

La causa raíz del exceso de mocking es que Claude Code intenta "hacer que los tests sean seguros de ejecutar." Todo lo que parece una dependencia externa se mockea.

Solución 1: Prohibir mocks explícitamente

This utility function has no side effects.
Do NOT use vi.mock(). Test it directly.

Solución 2: Hacer que Claude declare su estrategia de mocking primero

Before writing any tests, explain which dependencies you'll mock and why.
Wait for my approval before writing the implementation.

Solución 3: Añadir una regla de decisión de mocking a CLAUDE.md

markdown
## When to Mock

Mock these:
- Database connections
- External HTTP APIs
- File system operations
- Email/SMS sending

Do NOT mock these:
- Pure functions (input/output only, no side effects)
- Date processing (except Date.now())
- String and number operations
- Array and object manipulation

Una vez que esto está en CLAUDE.md, dejas de pelear la misma batalla en cada prompt. La regla se convierte en parte de la cultura de tu proyecto.


Conclusión

La clave para obtener buenos tests de Claude Code es ser explícito sobre qué no testear.

  • Escribe reglas de decisión de mocking en CLAUDE.md — consistencia en todo el proyecto
  • Incluye cuatro elementos en cada prompt (objetivo, restricciones, casos límite, nomenclatura) — la especificidad determina la calidad
  • Genera tests antes de la implementación — el bucle TDD asegura coherencia
  • Envía logs de fallos directamente a Claude — el ciclo de auto-corrección reduce drásticamente el tiempo de depuración
  • Aplica umbrales de cobertura en CI — construye el hábito de escribir tests

Deja de decir "escribe tests" y empieza a invertir en CLAUDE.md y diseño de prompts. La diferencia de calidad está en otra liga.