Testing con Vitest: lo mínimo para dormir tranquilo
Por Equipo Hexadevs · 14 ago 2026 · 3 min de lectura
Testear no es un lujo ni una tarea burocrática para code review. Es la diferencia entre debuggear a las 3 AM con un usuario gritándote, o enterarte vos en tu CI en 90 segundos.
Por qué Vitest y no Jest
Vitest usa el mismo motor de Vite que ya tenés en el proyecto. Si trabajás con Vite, Next.js o cualquier framework moderno, Vitest es lo que vas a terminar usando por una razón práctica: cero configuración, mismo dev server, mismos transforms. La API es casi idéntica a Jest, así que migrar es leer docs puntuales.
La pirámide que sí funciona
No testees todo con la misma intensidad:
- Unit tests: muchos, instantáneos, baratos. Funciones puras, utilidades, validación de esquemas.
- Tests de integración: algunos, velocidad media. Endpoints HTTP, queries a DB, componentes con su store.
- E2E: pocos, lentos, alto valor. Login, checkout, signup. Con Playwright o similar.
Cubrir unit + integration te resuelve el 90% de los bugs. El E2E cubre los flujos que no podés testear de otra forma.
Un test unitario decente
// sumar.ts
export function sumar(a: number, b: number): number {
return a + b
}
// sumar.test.ts
import { describe, it, expect } from "vitest"
import { sumar } from "./sumar"
describe("sumar", () => {
it("suma dos números positivos", () => {
expect(sumar(2, 3)).toBe(5)
})
it("suma números negativos", () => {
expect(sumar(-2, -3)).toBe(-5)
})
})
describe agrupa, it describe un caso, expect afirma. Esa es toda la sintaxis que necesitás para el 80% de los tests.
Tests de integración con Hono
import { describe, it, expect, beforeEach } from "vitest"
import { app } from "./app"
describe("POST /usuarios", () => {
it("crea un usuario con datos válidos", async () => {
const res = await app.request("/usuarios", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ nombre: "Ada", email: "ada@example.com" }),
})
expect(res.status).toBe(201)
const body = await res.json()
expect(body).toMatchObject({
nombre: "Ada",
email: "ada@example.com",
})
expect(body.id).toBeDefined()
})
it("rechaza emails duplicados con 409", async () => {
// crear uno primero
await app.request("/usuarios", { /* ... */ })
const res = await app.request("/usuarios", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ nombre: "Otro", email: "ada@example.com" }),
})
expect(res.status).toBe(409)
})
})
Acá ya estás testeando la API entera sin levantar un server real. Eso es lo que vale la pena.
Setup y mocks solo cuando hace falta
import { vi, beforeEach } from "vitest"
beforeEach(() => {
vi.restoreAllMocks()
})
const fetchMock = vi.fn()
vi.stubGlobal("fetch", fetchMock)
Mockear todo es una trampa: terminás testeando tus mocks. Mockeá solo lo que es I/O externo (red, tiempo, filesystem). Para lógica de negocio, testeá con datos reales.
Cobertura como guía, no como meta
Apuntar a 100% de cobertura es absurdo. La métrica útil es: ¿qué porcentaje de bugs reales que tuviste el año pasado los habría interceptado un test? Si la respuesta es baja, tenés un problema de qué testeás, no de cuánto.
Si tu CI tarda más de 5 minutos en correr los tests, tenés un problema de diseño de tests, no de velocidad de CI.
Tres reglas que usamos:
- Testeá el contrato, no la implementación. Si refactorizás la función pero el comportamiento es el mismo, el test no debe romperse.
- Un assert por concepto, varios por test está bien. Pero no asserts que prueban cosas distintas en el mismo
it. - Test names que cuenten una historia.
it("rechaza emails duplicados con 409")es mejor queit("works").
Con eso y una pipeline que corra en cada PR, tenés una red de seguridad que la mayoría de los proyectos reales todavía no tiene.