Vitest (backend)
Vitest no es exclusivo de frontend. Como runner para proyectos TypeScript y Node, sirve para probar servicios, repositorios y lógica de negocio de un backend Express. Esta guía cubre la configuración específica; para endpoints HTTP completos consulta Supertest.
Instalación
Sección titulada «Instalación»pnpm add -D vitestbun add -d vitestnpm install -D vitestimport { defineConfig } from "vitest/config"
export default defineConfig({ test: { environment: "node", include: ["src/**/*.test.ts"], setupFiles: ["./test/setup.ts"], restoreMocks: true }}){ "scripts": { "test": "vitest run", "test:watch": "vitest" }}Testear un service (sin tocar Express en absoluto)
Sección titulada «Testear un service (sin tocar Express en absoluto)»La lógica de negocio, aislada en services/ (consulta estructura MVC), se prueba directamente, sin levantar un servidor:
import { describe, expect, it, vi } from "vitest"import { usersRepository } from "../repositories/users.repository"import { usersService } from "./users.service"
vi.mock("../repositories/users.repository")
describe("usersService.obtenerUsuario", () => { it("lanza 404 si el usuario no existe", async () => { vi.mocked(usersRepository.findById).mockResolvedValue(null)
await expect(usersService.obtenerUsuario("id-inexistente")).rejects.toThrow( "Usuario no encontrado" ) })
it("devuelve el usuario si existe", async () => { vi.mocked(usersRepository.findById).mockResolvedValue({ id: "1", email: "a@b.com" })
const usuario = await usersService.obtenerUsuario("1") expect(usuario.email).toBe("a@b.com") })})vi.mock() reemplaza el repositorio real. Para dependencias importantes suele ser más explícito inyectar el repositorio al crear el servicio; así el test no depende del orden de carga de módulos.
const repository = { findById: vi.fn().mockResolvedValue(null)}const service = createUsersService({ repository })
await expect(service.getById("missing")).rejects.toMatchObject({ code: "USER_NOT_FOUND"})Comprueba un código o forma estable del error, no solo un mensaje que puede cambiar sin alterar el contrato.
Base de datos de test (para tests de integración del repository)
Sección titulada «Base de datos de test (para tests de integración del repository)»Para probar el repositorio —incluyendo query, tipos y constraints— necesitas una base real, normalmente aislada por suite o worker:
import { beforeEach } from "vitest"import { prisma } from "../src/libs/prisma"
beforeEach(async () => { await prisma.post.deleteMany() await prisma.user.deleteMany()})DATABASE_URL=postgresql://localhost:5432/mibase_test{ "scripts": { "test": "NODE_ENV=test vitest run" }}La limpieza debe respetar claves foráneas y paralelismo. Para suites grandes, considera un schema o contenedor por worker en lugar de eliminar todas las tablas antes de cada caso. La guía de integración backend desarrolla estas estrategias.
Probar errores del backend
Sección titulada «Probar errores del backend»Incluye timeout, dependencia rechazada, dato duplicado y operación abortada. En unitario, el fake produce el error; una prueba de integración confirma cómo llega desde la dependencia real.
repository.save.mockRejectedValue( Object.assign(new Error("duplicate"), { code: "UNIQUE_VIOLATION" }))
await expect(service.create(input)).rejects.toMatchObject({ code: "EMAIL_ALREADY_EXISTS"})El servicio traduce un detalle de infraestructura a un error del dominio. Conserva una integración para verificar que el adapter reconoce el código real del motor.
Resumen
Sección titulada «Resumen»| Nivel de test | Qué mockear |
|---|---|
| Service | Mockear el repository — no toca la base |
| Repository | Base de datos de test real, limpiada entre tests |
| Endpoint HTTP completo | Ver Supertest |
Consideraciones
Sección titulada «Consideraciones»environment: 'node'evita simular un DOM inexistente en backend.- Prueba servicios con dependencias controladas y repositorios con la base real.
- No compartas
DATABASE_URLcon desarrollo o producción. - Usa identificadores únicos, cleanup seguro y timeouts explícitos.