Testing sin dolor
Por que el testing importa para los Vibe Coders
Cuando haces vibe coding, la IA hace cambios rapido. Muy rapido. 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 tambien es un riesgo. Un solo cambio puede romper algo en una parte completamente diferente de tu app, y sin tests, no lo sabras hasta que un usuario lo reporte.
Los tests son tu red de seguridad. Atrapan bugs antes que los usuarios. Aqui esta por que importan especialmente para los vibe coders:
La IA hace cambios rapido — 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 aun pueden completar una compra. Te mueves rapido, y los tests evitan que te muevas rapido en la direccion equivocada.
Los tests son documentacion. Un test bien escrito te dice exactamente lo que una funcion o componente se supone que debe hacer. Cuando vuelves al codigo seis meses despues y te preguntas "que hace esto?", la suite de tests tiene la respuesta.
Los tests dan confianza para refactorizar. Quieres cambiar de una libreria de base de datos a otra? Reescribir tu capa de API? Migrar a una nueva version del framework? Si tienes buenos tests, refactorizas con audacia. Sin ellos, refactorizas con ansiedad.
Los tests ahorran tiempo a largo plazo. Si, escribir tests toma tiempo al inicio. Pero debuggear un problema en produccion a las 2 AM toma mas tiempo, cuesta mas dinero y arruina mas sueno. Los tests son una inversion que se paga cada vez.
Y aqui esta la mejor parte: la IA escribe tus tests por ti. Tu describes lo que debe probarse, y la IA genera el codigo del test. El testing nunca ha sido mas accesible.
La piramide de testing
No todos los tests son iguales. La piramide 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 rapidos (milisegundos), enfocados, y deberias tener la mayor cantidad de ellos.
Ejemplos:
- La funcion
calculateTotalsuma los precios correctamente? - La utilidad
formatDateretorna el string correcto? - El componente
Buttonrenderiza con el texto correcto?
Tests de integracion (El medio)
Los tests de integracion verifican como 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 envia datos y muestra un mensaje de exito?
- 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 deberias tener la menor cantidad, enfocados en flujos criticos de usuario.
Ejemplos:
- Puede un usuario registrarse, crear un proyecto e invitar a un companero de equipo?
- Puede un usuario agregar items al carrito, ingresar el pago y completar la compra?
- Puede un usuario admin iniciar sesion, ver reportes y exportar datos?
La proporcion correcta
Una suite de tests saludable se ve algo asi:
- 70% tests unitarios — rapidos, enfocados, atrapan bugs de logica
- 20% tests de integracion — verifican que las conexiones funcionen
- 10% tests E2E — validan viajes criticos del usuario
No te obsesiones con estos numeros. El punto es: muchos tests unitarios rapidos, algunos tests de integracion, y unos pocos tests E2E completos.
La IA escribe tus tests
Aqui es donde el vibe coding transforma el testing de una tarea tediosa en una conversacion. No necesitas conocer la sintaxis de testing, librerias de aserciones o frameworks de mocking. Describes lo que necesita probarse, y la IA escribe el codigo.
Prompt: "Escribe tests para la funcion calculateShipping. Debe retornar 0 para pedidos de mas de $50, $5.99 para envio estandar y $12.99 para envio express. Prueba casos extremos como exactamente $50, montos negativos y carrito vacio."
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()
})
})
Tu no escribiste un solo test. Describiste el comportamiento esperado, y la IA lo tradujo en codigo. Tu trabajo es revisar los tests, asegurarte de que cubren los casos correctos y ejecutarlos.
Vitest para tests unitarios y de integracion
Vitest es el test runner moderno para proyectos JavaScript y TypeScript. Es rapido, compatible con la API de Jest (asi que el vasto conocimiento de la IA sobre tests de Jest funciona aqui tambien), y se integra de maravilla con proyectos basados en Vite.
Configuracion
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
Configuracion
// 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 basica 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 mas facil 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 funcion 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 interactuan:
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: estas probando lo que el usuario ve (getByText, getByRole), no el estado interno. Esto hace que los tests sean resistentes a cambios de implementacion.
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()
Cuando mockear: APIs externas, bases de datos en tests unitarios, timers, generadores de numeros aleatorios. Cuando no mockear: Lo que realmente estas 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 aplicacion, desde la UI pasando por la API hasta la base de datos.
Configuracion
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 depuracion, ejecuta Playwright con un navegador visible:
npx playwright test --headed
Tambien puedes usar la UI de Playwright para depuracion paso a paso:
npx playwright test --ui
Esto abre una interfaz visual donde puedes avanzar paso a paso por cada accion, ver capturas de pantalla en cada paso e inspeccionar el DOM. Es increiblemente util para entender por que falla un test.
TDD con Vibe Coding
Test-Driven Development (TDD) sigue un ciclo simple: escribe un test que falle, implementa el codigo hasta que el test pase, luego refactoriza. Con IA, esto se vuelve aun mas poderoso.
El flujo de trabajo Vibe TDD
- Describe el comportamiento que quieres en un test
- Pidele a la IA que escriba el test fallido basandose en tu descripcion
- Pidele a la IA que implemente la funcion hasta que el test pase
- Repite para el siguiente comportamiento
Asi es como se ve una sesion real de TDD:
Tu: "Escribe un test para una funcion llamada calculateDiscount que aplique un 10% de descuento para pedidos de mas de $100, 20% para pedidos de mas de $500, y ningun descuento en caso contrario."
La IA escribe el test. Lo ejecutas — falla porque la funcion no existe aun.
Tu: "Ahora implementa calculateDiscount para que pase todos esos tests."
La IA escribe la funcion. Ejecutas los tests — pasan. Ahora tienes confianza de que la funcion trabaja correctamente porque los tests lo prueban.
Tu: "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 implementacion. Los tests pasan. Acabas de construir una funcionalidad con cobertura de tests completa en minutos.
Por que TDD funciona tan bien con IA
El TDD tradicional requiere que los desarrolladores escriban tests manualmente, lo cual muchos encuentran tedioso. Con IA, solo estas describiendo comportamiento en lenguaje natural. La IA maneja la sintaxis. Esto elimina la friccion que hace al TDD impopular mientras conserva todos los beneficios.
TDD tambien le da a la IA mejor contexto. Cuando la IA puede ver los tests, sabe exactamente lo que el codigo debe hacer. Genera implementaciones mas precisas porque el comportamiento esperado esta explicado en las aserciones.
Patrones de prompts orientados a testing
Estos prompts generan tests de alta calidad de manera confiable:
Test unitario para una funcion de utilidad: "Escribe tests para una funcion que calcula el costo de envio basado en peso y destino. Pesos bajo 1kg cuestan $5, 1-5kg cuestan $10, mas 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 contrasena, 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 validacion 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 busqueda por 300ms al escribir sin presionar Enter, muestra un boton de limpiar cuando el input tiene texto, y limpia el input cuando se hace clic en el boton de limpiar."
Cada prompt incluye comportamientos especificos y casos extremos. Mientras mas precisa sea tu descripcion, mejores seran los tests.
Cobertura como guia
La cobertura de codigo mide cuanto de tu codigo es ejercitado por los tests. Es un indicador util pero no una metrica de calidad.
Ejecutando reportes de cobertura
npx vitest run --coverage
Esto genera un reporte mostrando que lineas, ramas y funciones estan cubiertas por tus tests.
Que significan los numeros de cobertura
- 80% de cobertura es un objetivo razonable para la mayoria de proyectos. Significa que los caminos importantes estan probados.
- 100% de cobertura no significa que tu codigo esta libre de bugs. Significa que cada linea fue ejecutada durante los tests — pero los tests podrian no verificar las cosas correctas.
- Baja cobertura (menos del 50%) es una senal de advertencia. Grandes porciones de tu codigo no estan probadas, y los bugs probablemente se esconden ahi.
Cobertura como direccion, no como destino
Usa la cobertura para encontrar vacios. Si tu modulo de auth tiene 30% de cobertura, eso es un problema que vale la pena solucionar. Si una funcion 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 modulos con la cobertura mas baja. Escribe tests para llevar el modulo de auth del 30% a al menos 80% de cobertura."
Cuando saltarse los tests
No todo necesita tests. Aqui hay razones legitimas para saltarlos:
Prototipos verdaderos — Si estas construyendo un prototipo desechable para validar una idea, y lo reconstruiras desde cero si la idea funciona, salta los tests. Pero se honesto contigo mismo sobre si realmente es un prototipo.
Scripts de una sola vez — Un script de migracion que ejecutaras una vez y nunca volveras a tocar? Los tests serian excesivos.
Codigo exploratorio — Cuando estas experimentando con una nueva API o libreria, los tests te ralentizarian. Explora primero, prueba despues.
Pero siempre agrega tests antes de produccion. Si el codigo va a servir a usuarios reales, necesita tests. La linea es clara: el codigo de prototipo puede saltarse los tests. El codigo de produccion no.
Una regla practica: si te daria verguenza mostrar el codigo a un colega sin tests, agrega tests.
Flujo de trabajo de testing continuo
Los tests solo son utiles si los ejecutas. Aqui esta el flujo de trabajo que hace el testing automatico 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 codigo 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 critico. Si los tests fallan en CI pero el build continua 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 proteccion 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 metafora de la red de seguridad
Una red de seguridad solo funciona si esta desplegada. Ejecutar tests ocasionalmente es como tener una red que a veces esta debajo del trapecio. El flujo de trabajo debe ser: escribir codigo, 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."
Que sigue
Ahora tienes una estrategia de testing completa: tests unitarios con Vitest para retroalimentacion rapida, tests E2E con Playwright para validacion de viajes de usuario, y un pipeline de CI que ejecuta todo automaticamente. Sabes como usar la IA para escribir tests, como practicar TDD para funcionalidades complejas, y cuando los tests valen la inversion.
Esto cierra el Modulo III — las habilidades tecnicas core para construir aplicaciones de produccion. Has aprendido patrones de frontend, desarrollo backend, bases de datos, autenticacion y testing. En el Modulo IV, unimos todo construyendo proyectos reales de principio a fin. Aplicaras todo lo que has aprendido para crear aplicaciones completas y desplegables con la IA como tu companero de programacion.