Testing sin dolor
Por qué el testing importa para los Vibe Coders
Cuando haces vibe coding, la IA hace cambios rápido. Muy rápido. Puede refactorizar un componente completo, reescribir una consulta de base de datos o reestructurar un endpoint de API en segundos. Esa velocidad es un superpoder — pero también es un riesgo. Un solo cambio puede romper algo en una parte completamente diferente de tu app, y sin tests, no lo sabrás hasta que un usuario lo reporte.
Los tests son tu red de seguridad. Atrapan bugs antes que los usuarios. Aquí está por qué importan especialmente para los vibe coders:
La IA hace cambios rápido — los tests atrapan lo que los humanos pasan por alto. Cuando la IA reescribe tu flujo de checkout, los tests verifican que los usuarios existentes aún pueden completar una compra. Te mueves rápido, y los tests evitan que te muevas rápido en la dirección equivocada.
Los tests son documentación. Un test bien escrito te dice exactamente lo que una función o componente se supone que debe hacer. Cuando vuelves al código seis meses después y te preguntas "¿qué hace esto?", la suite de tests tiene la respuesta.
Los tests dan confianza para refactorizar. ¿Quieres cambiar de una librería de base de datos a otra? ¿Reescribir tu capa de API? ¿Migrar a una nueva versión del framework? Si tienes buenos tests, refactorizas con audacia. Sin ellos, refactorizas con ansiedad.
Los tests ahorran tiempo a largo plazo. Sí, escribir tests toma tiempo al inicio. Pero debuggear un problema en producción a las 2 AM toma más tiempo, cuesta más dinero y arruina más sueño. Los tests son una inversión que se paga cada vez.
Y aquí está la mejor parte: la IA escribe tus tests por ti. Tú describes lo que debe probarse, y la IA genera el código del test. El testing nunca ha sido más accesible.
La pirámide de testing
No todos los tests son iguales. La pirámide de testing te ayuda a equilibrar velocidad, alcance y confianza.
Tests unitarios (La base)
Los tests unitarios verifican funciones individuales y componentes de forma aislada. Son rápidos (milisegundos), enfocados, y deberías tener la mayor cantidad de ellos.
Ejemplos:
- ¿La función
calculateTotalsuma los precios correctamente? - ¿La utilidad
formatDateretorna el string correcto? - ¿El componente
Buttonrenderiza con el texto correcto?
Tests de integración (El medio)
Los tests de integración verifican cómo las piezas trabajan juntas. Son de velocidad media (segundos) y prueban las conexiones entre unidades.
Ejemplos:
- ¿La ruta de API valida la entrada y retorna la respuesta correcta?
- ¿El componente de formulario envía datos y muestra un mensaje de éxito?
- ¿La consulta de base de datos retorna los resultados esperados con filtros?
Tests end-to-end (La cima)
Los tests E2E simulan comportamiento real de usuario en un navegador real. Son lentos (segundos a minutos) y deberías tener la menor cantidad, enfocados en flujos críticos de usuario.
Ejemplos:
- ¿Puede un usuario registrarse, crear un proyecto e invitar a un compañero de equipo?
- ¿Puede un usuario agregar items al carrito, ingresar el pago y completar la compra?
- ¿Puede un usuario admin iniciar sesión, ver reportes y exportar datos?
La proporción correcta
Una suite de tests saludable se ve algo así:
- 70% tests unitarios — rápidos, enfocados, atrapan bugs de lógica
- 20% tests de integración — verifican que las conexiones funcionen
- 10% tests E2E — validan viajes críticos del usuario
No te obsesiones con estos números. El punto es: muchos tests unitarios rápidos, algunos tests de integración, y unos pocos tests E2E completos.
La IA escribe tus tests
Aquí es donde el vibe coding transforma el testing de una tarea tediosa en una conversación. No necesitas conocer la sintaxis de testing, librerías de aserciones o frameworks de mocking. Describes lo que necesita probarse, y la IA escribe el código.
Prompt: "Escribe tests para la función calculateShipping. Debe retornar 0 para pedidos de más de $50, $5.99 para envío estándar y $12.99 para envío express. Prueba casos extremos como exactamente $50, montos negativos y carrito vacío."
La IA genera:
import { describe, it, expect } from "vitest"
import { calculateShipping } from "./shipping"
describe("calculateShipping", () => {
it("retorna 0 para pedidos de mas de $50", () => {
expect(calculateShipping(75, "standard")).toBe(0)
expect(calculateShipping(50.01, "express")).toBe(0)
})
it("retorna $5.99 para envio estandar bajo $50", () => {
expect(calculateShipping(25, "standard")).toBe(5.99)
})
it("retorna $12.99 para envio express bajo $50", () => {
expect(calculateShipping(25, "express")).toBe(12.99)
})
it("retorna 0 para exactamente $50 (umbral de envio gratis)", () => {
expect(calculateShipping(50, "standard")).toBe(0)
})
it("lanza error para montos negativos", () => {
expect(() => calculateShipping(-10, "standard")).toThrow()
})
it("lanza error para carrito vacio (monto 0)", () => {
expect(() => calculateShipping(0, "standard")).toThrow()
})
})
Tú no escribiste un solo test. Describiste el comportamiento esperado, y la IA lo tradujo en código. Tu trabajo es revisar los tests, asegurarte de que cubren los casos correctos y ejecutarlos.
Vitest para tests unitarios y de integración
Vitest es el test runner moderno para proyectos JavaScript y TypeScript. Es rápido, compatible con la API de Jest (así que el vasto conocimiento de la IA sobre tests de Jest funciona aquí también), y se integra de maravilla con proyectos basados en Vite.
Configuración
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
Configuración
// vitest.config.ts
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import path from "path"
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./vitest.setup.ts"],
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})
// vitest.setup.ts
import "@testing-library/jest-dom/vitest"
Agrega un script de test a package.json:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}
Escribiendo tu primer test
La estructura básica usa bloques describe para agrupar tests relacionados, bloques it (o test) para casos de test individuales, y expect para aserciones:
import { describe, it, expect } from "vitest"
describe("utilidades matematicas", () => {
it("suma dos numeros correctamente", () => {
expect(add(2, 3)).toBe(5)
})
it("maneja numeros negativos", () => {
expect(add(-1, 1)).toBe(0)
})
})
Matchers comunes
Los matchers son las aserciones que usas para verificar valores:
// Igualdad exacta
expect(result).toBe(42)
// Igualdad profunda (objetos, arrays)
expect(user).toEqual({ name: "Alice", age: 30 })
// Veracidad
expect(isValid).toBeTruthy()
expect(error).toBeFalsy()
expect(nullValue).toBeNull()
expect(value).toBeDefined()
// Numeros
expect(price).toBeGreaterThan(0)
expect(count).toBeLessThanOrEqual(100)
expect(ratio).toBeCloseTo(0.3, 2)
// Strings
expect(message).toContain("success")
expect(email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
// Arrays
expect(items).toHaveLength(3)
expect(roles).toContain("admin")
// Errores
expect(() => divide(1, 0)).toThrow("Division by zero")
Testing de funciones de utilidad
Lo más fácil de probar — funciones puras con entradas y salidas claras:
import { describe, it, expect } from "vitest"
import { slugify, truncate, formatCurrency } from "@/lib/utils"
describe("slugify", () => {
it("convierte espacios en guiones", () => {
expect(slugify("Hello World")).toBe("hello-world")
})
it("elimina caracteres especiales", () => {
expect(slugify("Hello, World!")).toBe("hello-world")
})
it("maneja multiples espacios", () => {
expect(slugify("hello world")).toBe("hello-world")
})
})
describe("formatCurrency", () => {
it("formatea USD correctamente", () => {
expect(formatCurrency(1234.5, "USD")).toBe("$1,234.50")
})
it("maneja cero", () => {
expect(formatCurrency(0, "USD")).toBe("$0.00")
})
})
Testing de rutas de API
Puedes probar rutas de API de Next.js llamando a la función handler directamente:
import { describe, it, expect, vi } from "vitest"
import { POST } from "@/app/api/tasks/route"
describe("POST /api/tasks", () => {
it("crea una tarea con datos validos", async () => {
const request = new Request("http://localhost/api/tasks", {
method: "POST",
body: JSON.stringify({ title: "Comprar viveres", priority: "high" }),
headers: { "Content-Type": "application/json" },
})
const response = await POST(request)
const data = await response.json()
expect(response.status).toBe(201)
expect(data.data.title).toBe("Comprar viveres")
expect(data.data.priority).toBe("high")
})
it("retorna 400 si falta el titulo", async () => {
const request = new Request("http://localhost/api/tasks", {
method: "POST",
body: JSON.stringify({ priority: "high" }),
headers: { "Content-Type": "application/json" },
})
const response = await POST(request)
expect(response.status).toBe(400)
})
})
Testing de componentes React
Usando Testing Library, pruebas los componentes desde la perspectiva del usuario — lo que ven e interactúan:
import { describe, it, expect } from "vitest"
import { render, screen, fireEvent } from "@testing-library/react"
import { Counter } from "@/components/counter"
describe("Counter", () => {
it("renderiza con cuenta inicial de 0", () => {
render(<Counter />)
expect(screen.getByText("Count: 0")).toBeInTheDocument()
})
it("incrementa al hacer clic en el boton", () => {
render(<Counter />)
fireEvent.click(screen.getByRole("button", { name: /increment/i }))
expect(screen.getByText("Count: 1")).toBeInTheDocument()
})
it("acepta un prop de cuenta inicial", () => {
render(<Counter initialCount={10} />)
expect(screen.getByText("Count: 10")).toBeInTheDocument()
})
})
La idea clave: estás probando lo que el usuario ve (getByText, getByRole), no el estado interno. Esto hace que los tests sean resistentes a cambios de implementación.
Mocking
A veces necesitas reemplazar dependencias reales con sustitutos controlados:
import { describe, it, expect, vi } from "vitest"
// Mockear un modulo completo
vi.mock("@/lib/db", () => ({
db: {
task: {
findMany: vi.fn().mockResolvedValue([
{ id: "1", title: "Tarea 1" },
{ id: "2", title: "Tarea 2" },
]),
create: vi.fn().mockResolvedValue({ id: "3", title: "Nueva Tarea" }),
},
},
}))
// Mockear una sola funcion
const sendEmail = vi.fn()
Cuándo mockear: APIs externas, bases de datos en tests unitarios, timers, generadores de números aleatorios. Cuándo no mockear: Lo que realmente estás probando. El exceso de mocking lleva a tests que pasan pero no atrapan bugs reales.
Playwright para tests E2E
Playwright ejecuta un navegador real y simula acciones de usuario — hacer clic, escribir, navegar. Prueba toda tu pila de aplicación, desde la UI pasando por la API hasta la base de datos.
Configuración
npm install -D @playwright/test
npx playwright install
// playwright.config.ts
import { defineConfig } from "@playwright/test"
export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
webServer: {
command: "npm run dev",
port: 3000,
reuseExistingServer: !process.env.CI,
},
use: {
baseURL: "http://localhost:3000",
screenshot: "only-on-failure",
trace: "on-first-retry",
},
})
Escribiendo tu primer test E2E
// e2e/homepage.spec.ts
import { test, expect } from "@playwright/test"
test("la pagina principal carga y muestra la seccion hero", async ({ page }) => {
await page.goto("/")
await expect(page.getByRole("heading", { level: 1 })).toBeVisible()
await expect(page.getByRole("link", { name: /get started/i })).toBeVisible()
})
test("los enlaces de navegacion funcionan", async ({ page }) => {
await page.goto("/")
await page.click("text=Features")
await expect(page).toHaveURL("/features")
await expect(page.getByRole("heading", { name: /features/i })).toBeVisible()
})
Testing de flujos de usuario
Los tests E2E brillan al probar flujos de trabajo completos:
// e2e/task-management.spec.ts
import { test, expect } from "@playwright/test"
test("el usuario puede crear y eliminar una tarea", async ({ page }) => {
// Login
await page.goto("/login")
await page.fill('[name="email"]', "test@example.com")
await page.fill('[name="password"]', "password123")
await page.click('button[type="submit"]')
await expect(page).toHaveURL("/dashboard")
// Crear una tarea
await page.click("text=New Task")
await page.fill('[name="title"]', "Comprar viveres")
await page.fill('[name="description"]', "Leche, huevos, pan")
await page.click('button:has-text("Create")')
// Verificar que aparece
await expect(page.getByText("Comprar viveres")).toBeVisible()
// Eliminar la tarea
await page.getByText("Comprar viveres").hover()
await page.click('[aria-label="Delete task"]')
await page.click('button:has-text("Confirm")')
// Verificar que desaparecio
await expect(page.getByText("Comprar viveres")).not.toBeVisible()
})
Testing de layouts responsivos
Playwright puede emular diferentes dispositivos:
test("la navegacion movil muestra el menu hamburguesa", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 })
await page.goto("/")
// El nav de desktop debe estar oculto
await expect(page.getByRole("navigation").locator(".desktop-menu")).not.toBeVisible()
// El boton hamburguesa debe ser visible
const hamburger = page.getByRole("button", { name: /menu/i })
await expect(hamburger).toBeVisible()
// Hacer clic en hamburguesa para abrir menu movil
await hamburger.click()
await expect(page.getByRole("navigation").locator(".mobile-menu")).toBeVisible()
})
Ejecutando tests en modo visible
Para depuración, ejecuta Playwright con un navegador visible:
npx playwright test --headed
También puedes usar la UI de Playwright para depuración paso a paso:
npx playwright test --ui
Esto abre una interfaz visual donde puedes avanzar paso a paso por cada acción, ver capturas de pantalla en cada paso e inspeccionar el DOM. Es increíblemente útil para entender por qué falla un test.
TDD con Vibe Coding
Test-Driven Development (TDD) sigue un ciclo simple: escribe un test que falle, implementa el código hasta que el test pase, luego refactoriza. Con IA, esto se vuelve aún más poderoso.
El flujo de trabajo Vibe TDD
- Describe el comportamiento que quieres en un test
- Pídele a la IA que escriba el test fallido basándose en tu descripción
- Pídele a la IA que implemente la función hasta que el test pase
- Repite para el siguiente comportamiento
Así es como se ve una sesión real de TDD:
Tú: "Escribe un test para una función llamada calculateDiscount que aplique un 10% de descuento para pedidos de más de $100, 20% para pedidos de más de $500, y ningún descuento en caso contrario."
La IA escribe el test. Lo ejecutas — falla porque la función no existe aún.
Tú: "Ahora implementa calculateDiscount para que pase todos esos tests."
La IA escribe la función. Ejecutas los tests — pasan. Ahora tienes confianza de que la función trabaja correctamente porque los tests lo prueban.
Tú: "Agrega un test para un cliente VIP que obtiene un 5% extra encima del descuento regular."
La IA agrega el test. Falla. La IA actualiza la implementación. Los tests pasan. Acabas de construir una funcionalidad con cobertura de tests completa en minutos.
Por qué TDD funciona tan bien con IA
El TDD tradicional requiere que los desarrolladores escriban tests manualmente, lo cual muchos encuentran tedioso. Con IA, solo estás describiendo comportamiento en lenguaje natural. La IA maneja la sintaxis. Esto elimina la fricción que hace al TDD impopular mientras conserva todos los beneficios.
TDD también le da a la IA mejor contexto. Cuando la IA puede ver los tests, sabe exactamente lo que el código debe hacer. Genera implementaciones más precisas porque el comportamiento esperado está explicado en las aserciones.
Patrones de prompts orientados a testing
Estos prompts generan tests de alta calidad de manera confiable:
Test unitario para una función de utilidad: "Escribe tests para una función que calcula el costo de envío basado en peso y destino. Pesos bajo 1kg cuestan $5, 1-5kg cuestan $10, más de 5kg cuestan $15. Destinos internacionales agregan un recargo de $20. Prueba casos extremos: exactamente 1kg, exactamente 5kg, peso cero (debe lanzar error), peso negativo (debe lanzar error)."
Test E2E para un flujo de usuario: "Escribe un test E2E con Playwright que verifique que un usuario puede registrarse con email y contraseña, crear un nuevo proyecto llamado 'Mi Proyecto', agregar una tarea al proyecto, marcar la tarea como completada, y luego eliminar el proyecto."
Tests de endpoints de API: "Escribe tests con Vitest para el endpoint /api/tasks cubriendo todas las operaciones CRUD: crear una tarea (POST), listar todas las tareas (GET), actualizar una tarea (PUT) y eliminar una tarea (DELETE). Prueba errores de validación para campos requeridos faltantes y respuestas 404 para tareas que no existen."
Test de componente React: "Escribe tests para un componente SearchBar que: renderiza un campo de input, llama onSearch con la consulta cuando el usuario escribe y presiona Enter, aplica debounce a la búsqueda por 300ms al escribir sin presionar Enter, muestra un botón de limpiar cuando el input tiene texto, y limpia el input cuando se hace clic en el botón de limpiar."
Cada prompt incluye comportamientos específicos y casos extremos. Mientras más precisa sea tu descripción, mejores serán los tests.
Cobertura como guía
La cobertura de código mide cuánto de tu código es ejercitado por los tests. Es un indicador útil pero no una métrica de calidad.
Ejecutando reportes de cobertura
npx vitest run --coverage
Esto genera un reporte mostrando qué líneas, ramas y funciones están cubiertas por tus tests.
Qué significan los números de cobertura
- 80% de cobertura es un objetivo razonable para la mayoría de proyectos. Significa que los caminos importantes están probados.
- 100% de cobertura no significa que tu código está libre de bugs. Significa que cada línea fue ejecutada durante los tests — pero los tests podrían no verificar las cosas correctas.
- Baja cobertura (menos del 50%) es una señal de advertencia. Grandes porciones de tu código no están probadas, y los bugs probablemente se esconden ahí.
Cobertura como dirección, no como destino
Usa la cobertura para encontrar vacíos. Si tu módulo de auth tiene 30% de cobertura, eso es un problema que vale la pena solucionar. Si una función de utilidad tiene 95% de cobertura y el 5% faltante es un handler de error inalcanzable, no lo persigas.
Prompt: "Ejecuta el reporte de cobertura de tests e identifica los módulos con la cobertura más baja. Escribe tests para llevar el módulo de auth del 30% a al menos 80% de cobertura."
Cuándo saltarse los tests
No todo necesita tests. Aquí hay razones legítimas para saltarlos:
Prototipos verdaderos — Si estás construyendo un prototipo desechable para validar una idea, y lo reconstruirás desde cero si la idea funciona, salta los tests. Pero sé honesto contigo mismo sobre si realmente es un prototipo.
Scripts de una sola vez — ¿Un script de migración que ejecutarás una vez y nunca volverás a tocar? Los tests serían excesivos.
Código exploratorio — Cuando estás experimentando con una nueva API o librería, los tests te ralentizarían. Explora primero, prueba después.
Pero siempre agrega tests antes de producción. Si el código va a servir a usuarios reales, necesita tests. La línea es clara: el código de prototipo puede saltarse los tests. El código de producción no.
Una regla práctica: si te daría vergüenza mostrar el código a un colega sin tests, agrega tests.
Flujo de trabajo de testing continuo
Los tests solo son útiles si los ejecutas. Aquí está el flujo de trabajo que hace el testing automático y sin esfuerzo.
Ejecutar tests antes de hacer commit
Agrega un hook pre-commit que ejecute los tests:
npx vitest run --reporter=verbose
Si los tests fallan, el commit se bloquea. Esto previene que código roto entre a tu codebase.
Ejecutar tests en CI
Configura tu pipeline de CI (GitHub Actions, por ejemplo) para ejecutar tests en cada pull request:
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx vitest run --coverage
- run: npx playwright install --with-deps
- run: npx playwright test
Fallar el build cuando fallen los tests
Esto es crítico. Si los tests fallan en CI pero el build continúa de todas formas, has perdido la red de seguridad. Configura tu CI para bloquear merges cuando los tests fallen.
En GitHub, habilita las reglas de protección de branch:
- Ve a Settings > Branches > Branch protection rules
- Requiere que las verificaciones de estado pasen antes de mergear
- Selecciona tu workflow de tests
La metáfora de la red de seguridad
Una red de seguridad solo funciona si está desplegada. Ejecutar tests ocasionalmente es como tener una red que a veces está debajo del trapecio. El flujo de trabajo debe ser: escribir código, ejecutar tests, hacer commit, push, CI ejecuta tests de nuevo, merge. Sin huecos en la red.
Prompt: "Configura un workflow de GitHub Actions que ejecute tests unitarios de Vitest y tests E2E de Playwright en cada push y pull request. Falla el build si cualquier test falla. Incluye un reporte de cobertura."
Qué sigue
Ahora tienes una estrategia de testing completa: tests unitarios con Vitest para retroalimentación rápida, tests E2E con Playwright para validación de viajes de usuario, y un pipeline de CI que ejecuta todo automáticamente. Sabes cómo usar la IA para escribir tests, cómo practicar TDD para funcionalidades complejas, y cuándo los tests valen la inversión.
Esto cierra el Módulo III — las habilidades técnicas core para construir aplicaciones de producción. Has aprendido patrones de frontend, desarrollo backend, bases de datos, autenticación y testing. En el Módulo IV, unimos todo construyendo proyectos reales de principio a fin. Aplicarás todo lo que has aprendido para crear aplicaciones completas y desplegables con la IA como tu compañero de programación.