De Contenedor Único a Stack Completo

En la lección anterior, aprendiste a ejecutar tests en un solo contenedor Docker. Pero las aplicaciones reales raramente se ejecutan aisladas. Una aplicación web típica necesita una base de datos, un caché, posiblemente una cola de mensajes y quizás un servicio de email. Docker Compose te permite definir todo esto como un solo stack que inicia y se detiene junto.

Para QA, Docker Compose es transformador. En lugar de configurar manualmente PostgreSQL, Redis y la aplicación antes de ejecutar tests de integración, defines todo en un archivo docker-compose.yml y lo inicias con un solo comando.

Fundamentos de Docker Compose

Ejemplo Mínimo

version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://test:test@db:5432/testdb
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started

  db:
    image: postgres:15
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U test"]
      interval: 5s
      timeout: 5s
      retries: 5

  cache:
    image: redis:7-alpine

Iniciando y Deteniendo

# Iniciar todos los servicios
docker compose up -d

# Iniciar y reconstruir imágenes
docker compose up -d --build

# Ver logs
docker compose logs -f

# Detener y eliminar contenedores
docker compose down

# Detener, eliminar contenedores Y volúmenes (estado limpio)
docker compose down -v

Patrones de Entornos de Test

Patrón 1: App + Dependencias + Test Runner

El patrón más común para testing de integración y E2E:

version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=test
      - DATABASE_URL=postgresql://test:test@db:5432/testdb
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:15
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
    volumes:
      - ./scripts/init-test-db.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U test"]
      interval: 5s
      timeout: 5s
      retries: 5

  tests:
    build:
      context: .
      dockerfile: Dockerfile.tests
    environment:
      - BASE_URL=http://app:3000
      - DATABASE_URL=postgresql://test:test@db:5432/testdb
    depends_on:
      app:
        condition: service_started
    volumes:
      - ./test-results:/app/test-results

Patrón 2: Stack de Integración Completo

version: '3.8'

services:
  app:
    build: .
    environment:
      - DATABASE_URL=postgresql://test:test@db:5432/testdb
      - REDIS_URL=redis://cache:6379
      - MAIL_HOST=mailhog
      - MAIL_PORT=1025
      - S3_ENDPOINT=http://localstack:4566
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:15
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U test"]
      interval: 5s
      timeout: 5s
      retries: 5

  cache:
    image: redis:7-alpine

  mailhog:
    image: mailhog/mailhog
    ports:
      - "8025:8025"

  localstack:
    image: localstack/localstack
    environment:
      - SERVICES=s3
      - DEFAULT_REGION=us-east-1

Health Checks

Los health checks son esenciales para la confiabilidad de los tests. Sin ellos, los tests pueden iniciar antes de que la base de datos esté lista para aceptar conexiones.

db:
  image: postgres:15
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U test"]
    interval: 5s
    timeout: 5s
    retries: 5
    start_period: 10s

Ejercicio: Construye un Entorno de Test Completo

Diseña un docker-compose.yml para una aplicación de e-commerce con:

  • Backend API Node.js
  • Base de datos PostgreSQL (con datos semilla)
  • Caché Redis
  • Servicio de email (para confirmaciones de pedido)
  • Test runner para tests API y E2E
  • Resultados de test guardados en la máquina host
Solución
version: '3.8'

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=test
      - DATABASE_URL=postgresql://test:test@db:5432/ecommerce_test
      - REDIS_URL=redis://cache:6379
      - MAIL_HOST=mailhog
      - MAIL_PORT=1025
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 5s
      timeout: 5s
      retries: 10

  db:
    image: postgres:15
    environment:
      POSTGRES_DB: ecommerce_test
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
    volumes:
      - ./scripts/seed-test-data.sql:/docker-entrypoint-initdb.d/01-seed.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U test"]
      interval: 5s
      timeout: 5s
      retries: 5

  cache:
    image: redis:7-alpine

  mailhog:
    image: mailhog/mailhog
    ports:
      - "8025:8025"

  api-tests:
    build:
      context: .
      dockerfile: Dockerfile.tests
    environment:
      - BASE_URL=http://api:3000
      - MAILHOG_URL=http://mailhog:8025
    command: npm run test:api
    depends_on:
      api:
        condition: service_healthy
    volumes:
      - ./test-results/api:/app/test-results

  e2e-tests:
    image: mcr.microsoft.com/playwright:v1.40.0-focal
    working_dir: /app
    volumes:
      - .:/app
      - ./test-results/e2e:/app/test-results
    environment:
      - BASE_URL=http://api:3000
    command: npx playwright test
    depends_on:
      api:
        condition: service_healthy

Uso:

# Ejecutar tests de API
docker compose run --rm api-tests

# Ejecutar tests E2E
docker compose run --rm e2e-tests

# Limpiar
docker compose down -v

Mejores Prácticas

  1. Siempre usa health checks con depends_on: condition: service_healthy. El orden de inicio solo no es suficiente.

  2. Usa docker compose down -v después de los tests. El flag -v elimina volúmenes, asegurando que cada ejecución inicie con estado limpio.

  3. Monta resultados de tests como volúmenes. Esto hace los reportes accesibles en la máquina host y en artefactos CI.

  4. Mantén datos de test en archivos SQL semilla montados vía docker-entrypoint-initdb.d/. Esto asegura datos de test consistentes y reproducibles.

  5. Usa docker compose run --rm para ejecución de tests. El flag --rm elimina el contenedor después de que termine.

  6. Fija versiones de imágenes. Usa postgres:15 en lugar de postgres:latest para evitar cambios de comportamiento inesperados.