Software

Testing de integración con Testcontainers: adiós a los mocks frágiles

Los mocks de base de datos dan falsa seguridad: el test pasa pero el código falla en producción. Testcontainers levanta contenedores Docker reales durante los tests, eliminando la brecha entre el entorno de test y producción.

N-Byte
7 min lectura

Los mocks de base de datos son cómodos hasta que dejan de serlo. Mockar un repositorio significa que estás testeando que tu código llama al método correcto del mock, no que tu código funciona correctamente con una base de datos real. Las diferencias entre el comportamiento del mock y el comportamiento real del motor de base de datos —transacciones, constraints, índices, comportamiento de tipos— son exactamente donde ocurren los bugs que los tests no detectan.

Testcontainers resuelve esto levantando contenedores Docker reales (PostgreSQL, Redis, MongoDB, Kafka, lo que necesites) como parte de la suite de tests, y eliminándolos al terminar.

Cómo funciona

Testcontainers tiene librerías para Node.js, Python, Java, Go y otros lenguajes. El principio es el mismo: defines qué contenedor necesitas, el test framework lo levanta antes del suite, tu código lo usa como si fuera la base de datos de producción, y todo se elimina al finalizar.

import { PostgreSqlContainer } from "@testcontainers/postgresql"
import { PrismaClient } from "@prisma/client"
 
describe("UserRepository (integration)", () => {
  let container: StartedPostgreSqlContainer
  let prisma: PrismaClient
 
  beforeAll(async () => {
    // Levanta un contenedor PostgreSQL real
    container = await new PostgreSqlContainer("postgres:16")
      .withDatabase("testdb")
      .withUsername("testuser")
      .withPassword("testpass")
      .start()
 
    // Conecta Prisma al contenedor
    prisma = new PrismaClient({
      datasources: {
        db: { url: container.getConnectionUri() }
      }
    })
 
    // Ejecuta las migraciones reales contra este PostgreSQL
    await prisma.$executeRaw`CREATE TABLE users (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      email TEXT UNIQUE NOT NULL,
      created_at TIMESTAMPTZ DEFAULT NOW()
    )`
  }, 60_000) // timeout generoso para pull de imagen
 
  afterAll(async () => {
    await prisma.$disconnect()
    await container.stop()
  })
 
  it("guarda y recupera un usuario correctamente", async () => {
    await prisma.user.create({
      data: { email: "test@example.com" }
    })
 
    const user = await prisma.user.findUnique({
      where: { email: "test@example.com" }
    })
 
    expect(user).not.toBeNull()
    expect(user?.email).toBe("test@example.com")
  })
 
  it("lanza error al insertar email duplicado", async () => {
    await prisma.user.create({ data: { email: "dup@test.com" } })
 
    await expect(
      prisma.user.create({ data: { email: "dup@test.com" } })
    ).rejects.toThrow()
  })
})

El segundo test es la clave

Nota el segundo test: verifica que el constraint de unicidad lanza un error. Con un mock, este test probablemente pasaría si simplemente configuras el mock para que rechace, pero no estarías probando que el constraint existe en el esquema. Con Testcontainers, el constraint lo valida PostgreSQL real —exactamente como en producción.

Consideraciones de velocidad

El primer beforeAll tarda más de lo usual porque descarga la imagen Docker si no está en caché localmente. En CI, esto puede ser un problema si cada suite levanta su propio contenedor. Soluciones:

  • Reutilizar el contenedor entre suites con Testcontainers ryuk-disabled y conexión compartida
  • Pre-pull de imágenes en CI como paso separado del pipeline
  • Paralelizar las suites en CI para que el tiempo de setup se diluya

En máquinas de desarrollo con la imagen en caché, el startup de un contenedor PostgreSQL toma 1-3 segundos —perfectamente aceptable.

Cuándo usar Testcontainers vs mocks

Testcontainers para:

  • Lógica que depende de comportamientos específicos del motor (transacciones, constraints, queries complejos)
  • Tests de repositorios y servicios de persistencia
  • Verificar que las migraciones producen el esquema esperado

Mocks para:

  • Aislar la lógica de negocio de la infraestructura en tests unitarios
  • Tests de casos de uso donde el repositorio es un detalle que no importa probar
  • Tests que deben ser instantáneos y no pueden depender de Docker

No es una elección binaria: una suite bien diseñada tiene ambos, cada uno donde agrega más valor.

Recibe artículos de Software