Supertest
Vitest ejecuta la suite y prueba funciones aisladas. Supertest envía solicitudes contra la aplicación Express, atravesando middlewares, routing y manejo de errores sin necesitar un puerto público.
Instalación
Sección titulada «Instalación»pnpm add -D supertestpnpm add -D @types/supertestbun add -d supertestbun add -d @types/supertestnpm install -D supertestnpm install -D @types/supertestRequisito: exportar app sin llamar a .listen()
Sección titulada «Requisito: exportar app sin llamar a .listen()»import express from "express"
const app = express()app.use(express.json())// ... middlewares y rutas ...
export default app // exportar SIN listen()import app from "./app"
app.listen(3000, () => console.log("Escuchando en 3000"))Separar app.ts (la configuración de Express) de server.ts (el .listen() real) es lo que permite que Supertest use la misma app sin abrir un puerto de verdad — Supertest simula las requests directamente contra el objeto app.
También mejora el cierre del servidor y evita que importar un módulo inicie efectos inesperados durante los tests.
Un test de integración
Sección titulada «Un test de integración»import request from "supertest"import { describe, expect, it } from "vitest"import app from "../app"
describe("POST /usuarios", () => { it("crea un usuario con datos válidos", async () => { const response = await request(app) .post("/usuarios") .send({ email: "nuevo@test.com", password: "password123" })
expect(response.status).toBe(201) expect(response.body.email).toBe("nuevo@test.com") })
it("rechaza un email inválido con 400", async () => { const response = await request(app) .post("/usuarios") .send({ email: "no-es-un-email", password: "password123" })
expect(response.status).toBe(400) expect(response.body.error).toBeDefined() })})request(app) arma la request; .post(ruta), .get(ruta), etc. definen el verbo; .send(body) manda el body; el resultado (response) tiene .status, .body, .headers ya parseados.
Comprueba el contrato completo que importa, no solo status:
expect(response.headers["content-type"]).toMatch(/application\/json/)expect(response.body).toMatchObject({ id: expect.any(String), email: "nuevo@test.com"})expect(response.body).not.toHaveProperty("passwordHash")Testear rutas protegidas (con auth)
Sección titulada «Testear rutas protegidas (con auth)»describe("GET /perfil", () => { it("rechaza sin token", async () => { const response = await request(app).get("/perfil") expect(response.status).toBe(401) })
it("devuelve el perfil con un token válido", async () => { const token = firmarToken({ sub: "user-1", rol: "user" }) // helper de test, mismo JWT real
const response = await request(app) .get("/perfil") .set("Authorization", `Bearer ${token}`)
expect(response.status).toBe(200) expect(response.body.userId).toBe("user-1") })}).set('Authorization', ...) agrega headers — el mismo patrón para cookies (.set('Cookie', 'token=...')) o cualquier header custom que la API espere.
Mantener cookies entre solicitudes
Sección titulada «Mantener cookies entre solicitudes»request.agent(app) conserva cookies y permite probar una sesión:
const agent = request.agent(app)
await agent .post("/login") .send({ email: user.email, password: "correct-password" }) .expect(204)
const profile = await agent.get("/profile").expect(200)expect(profile.body.id).toBe(user.id)Usa una cuenta distinta por caso o reinicia el agente. Prueba atributos de cookie relevantes —HttpOnly, Secure, SameSite— cuando forman parte del control de seguridad.
Errores y archivos
Sección titulada «Errores y archivos»await request(app) .post("/avatar") .attach("file", Buffer.from("not-an-image"), "avatar.txt") .expect(415)Incluye body ausente, JSON inválido, tamaño excesivo, tipo de contenido incorrecto y error del servicio. Confirma que el error mantiene una forma estable y no filtra stack traces.
Resumen
Sección titulada «Resumen»| Método | Qué hace |
|---|---|
request(app) | Punto de entrada, usa la app de Express sin .listen() |
.get/.post/.put/.delete(ruta) | Define el verbo y la ruta |
.send(body) | Manda el body (JSON automáticamente si es un objeto) |
.set(header, valor) | Headers custom, auth, cookies |
response.status / .body | La respuesta completa, ya parseada |
Consideraciones
Sección titulada «Consideraciones»- Supertest testea la app entera “de afuera hacia adentro” — es el nivel correcto para verificar que middlewares, validación y error handler realmente funcionan juntos, algo que un test de service aislado (con todo mockeado) no puede confirmar.
- Para rutas que tocan la base de datos real, combinar con la base de test de Vitest — sin eso, estos tests fallarían (o peor, escribirían) contra la base de desarrollo/producción.
- Supertest no reemplaza una prueba contra el servidor desplegado: proxy, TLS, adapter y límites de plataforma quedan fuera.
- Cierra conexiones, workers y timers en teardown para que Vitest no quede abierto.