Como usar Docker para testes locais rápidos

1. Por que Docker é ideal para testes locais rápidos

O Docker transforma a experiência de testes locais ao oferecer isolamento completo de dependências e ambientes perfeitamente replicáveis. Em vez de instalar bancos de dados, filas de mensagens e outras ferramentas diretamente no sistema operacional do desenvolvedor, cada componente roda em seu próprio container, garantindo que o ambiente de teste seja idêntico ao de produção.

O problema clássico "funciona na minha máquina" desaparece porque o Docker empacota não apenas o código, mas também todas as bibliotecas, versões de runtime e configurações necessárias. Além disso, o ciclo de build, teste e destruição é extremamente rápido: containers podem ser criados, executados e removidos em segundos, sem deixar resíduos no sistema hospedeiro. Essa natureza stateless permite que cada execução de teste comece com um ambiente limpo e previsível.

2. Configurando um ambiente de teste mínimo com Docker Compose

O Docker Compose é a ferramenta ideal para orquestrar múltiplos containers que simulam o ambiente completo de testes. Um arquivo docker-compose.yml básico para testes pode incluir serviços como banco de dados, cache e a própria aplicação:

version: '3.8'

services:
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: testuser
      POSTGRES_PASSWORD: testpass
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  app:
    build:
      context: .
      dockerfile: Dockerfile.test
    environment:
      DATABASE_URL: postgresql://testuser:testpass@db:5432/testdb
      REDIS_URL: redis://redis:6379/0
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - .:/app
    command: ["npm", "test"]

Variáveis de ambiente tornam a configuração flexível, permitindo que o mesmo arquivo funcione em diferentes contextos (CI, desenvolvimento local, produção) apenas alterando os valores externamente.

3. Executando testes unitários dentro do container

Um Dockerfile otimizado para testes utiliza construção em múltiplos estágios (multistage build) para separar dependências de desenvolvimento das de produção:

# Estágio 1: Instalação de dependências
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=development

# Estágio 2: Testes
FROM node:18-alpine AS test
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
CMD ["npm", "test"]

Para rodar os testes sem modificar o ambiente local, execute:

docker compose up --build --abort-on-container-exit

A montagem de volumes (volumes: - .:/app) permite hot-reload: alterações no código são refletidas imediatamente dentro do container, sem necessidade de reconstruir a imagem a cada modificação.

4. Testes de integração com dependências externas

Testes de integração exigem que serviços como PostgreSQL e Redis estejam disponíveis e saudáveis. O uso de depends_on com condition: service_healthy garante que o container da aplicação só inicie quando o banco estiver pronto para receber conexões:

services:
  app:
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

Para simular APIs externas, utilize ferramentas como WireMock ou MockServer em containers separados. A limpeza automática de dados entre execuções pode ser feita com volumes temporários:

services:
  db:
    volumes:
      - test_db_data:/var/lib/postgresql/data

volumes:
  test_db_data:

Cada execução de docker compose down -v remove os volumes, garantindo um estado inicial limpo.

5. Estratégias para testes de aceitação e E2E

Testes de aceitação e ponta a ponta (E2E) se beneficiam de containers temporários que simulam o ambiente completo de produção. Use redes Docker para isolar a comunicação entre serviços:

networks:
  test_network:
    driver: bridge

services:
  app:
    networks:
      - test_network
  cypress:
    image: cypress/included:latest
    networks:
      - test_network
    environment:
      CYPRESS_BASE_URL: http://app:3000
    volumes:
      - ./cypress:/e2e
      - ./cypress/reports:/reports

Dentro do container de testes E2E, é possível gerar relatórios e screenshots:

command: >
  sh -c "cypress run --spec 'cypress/e2e/**/*.cy.js' --reporter junit --reporter-options 'mochaFile=/reports/test-results.xml'"

6. Dicas para acelerar o ciclo de teste

O cache de camadas Docker é uma das maiores vantagens para acelerar builds. Organize o Dockerfile para que as instruções que mudam com menos frequência (instalação de dependências) fiquem no início:

COPY package*.json ./
RUN npm ci
COPY . .

Use --build-arg para parametrizar versões de dependências sem invalidar o cache:

docker compose build --build-arg NODE_VERSION=18

Compartilhe imagens base entre projetos usando um registry privado ou imagens públicas otimizadas. Automatize comandos frequentes com scripts de shell:

#!/bin/bash
# test.sh
docker compose up -d db redis
sleep 2
docker compose run --rm app npm run test:integration
docker compose down

7. Tratamento de falhas e depuração em testes

Quando um teste falha, a depuração eficiente é crucial. Use docker logs para visualizar logs centralizados:

docker compose logs app

Para inspecionar o estado de um container sem encerrá-lo, utilize docker pause:

docker compose pause app
# Inspecione o container com docker exec -it app sh
docker compose unpause app

Snapshots de estado podem ser criados com volumes nomeados. Antes de um teste crítico, faça backup do volume:

docker run --rm -v test_db_data:/source -v $(pwd):/backup alpine tar czf /backup/db_snapshot.tar.gz -C /source .

Para restaurar:

docker run --rm -v test_db_data:/target -v $(pwd):/backup alpine tar xzf /backup/db_snapshot.tar.gz -C /target

8. Boas práticas e armadilhas comuns

Evite expor portas desnecessárias durante os testes. Em vez de mapear portas para o host, utilize a rede interna do Docker:

services:
  db:
    expose:
      - "5432"
    # Sem ports: para testes locais

Gerencie recursos de CPU e memória para evitar que containers de teste consumam todo o sistema:

services:
  app:
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

Versionamento de imagens é essencial para rastreabilidade. Use tags semânticas:

docker build -t myapp-test:1.2.3 .

Para variações locais sem modificar o docker-compose.yml principal, utilize docker-compose.override.yml. Este arquivo é automaticamente mesclado e permite ajustes como montagem de volumes extras ou variáveis de ambiente específicas do desenvolvedor.

Referências

  • Documentação Oficial do Docker Compose — Guia completo sobre orquestração de containers com Docker Compose, incluindo exemplos de configuração para ambientes de teste.
  • Dockerfile Best Practices — Recomendações oficiais para escrever Dockerfiles eficientes, com foco em cache de camadas e construção em múltiplos estágios.
  • Testing Strategies with Docker — Artigo técnico sobre estratégias de teste utilizando containers, abordando testes unitários, integração e E2E.
  • Cypress Docker Documentation — Guia oficial para execução de testes E2E com Cypress em containers Docker, incluindo configuração de redes e geração de relatórios.
  • PostgreSQL Docker Healthcheck — Exemplos de healthchecks para PostgreSQL em containers, essenciais para sincronização de dependências em testes de integração.
  • WireMock Docker Image — Documentação do WireMock para simulação de APIs em containers, útil para testes de integração com serviços externos.
  • Docker Resource Constraints — Guia oficial sobre limitação de CPU e memória em containers, importante para evitar sobrecarga durante testes locais.