Estratégias de testes em arquiteturas de microservices

1. Fundamentos dos Testes em Microsserviços

A pirâmide de testes clássica, proposta por Mike Cohn, precisa ser adaptada para arquiteturas de microsserviços. Em vez de uma pirâmide com três camadas, trabalhamos com cinco: testes unitários, de componente, de integração, de contrato e ponta a ponta (E2E). Cada camada possui granularidade e custo de manutenção distintos.

Os desafios específicos incluem dependências de rede, latência variável, consistência eventual e estados distribuídos. Um microsserviço raramente opera isolado — ele consome APIs, filas de mensagens e bancos de dados que podem falhar ou responder com atraso. O trade-off fundamental está entre isolar completamente o serviço (testes rápidos, mas menos realistas) e testar com dependências reais (mais fidelidade, porém maior fragilidade e tempo de execução).

2. Testes Unitários e de Componente

Testes unitários cobrem a lógica de negócio pura: validação de regras, cálculos e transformações de dados. Para microsserviços, a mockagem de dependências externas é essencial. Utilizamos stubs para retornar respostas predefinidas, mocks para verificar interações e fakes para simular comportamentos mais complexos.

Exemplo de teste unitário para um handler HTTP em Node.js:

// Teste unitário de handler com mock do serviço de pagamento
const handler = require('./paymentHandler');
const paymentService = require('./paymentService');

jest.mock('./paymentService');

test('deve processar pagamento com sucesso', async () => {
  paymentService.process.mockResolvedValue({ status: 'approved' });
  const req = { body: { amount: 100, currency: 'USD' } };
  const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
  await handler(req, res);
  expect(res.status).toHaveBeenCalledWith(200);
  expect(res.json).toHaveBeenCalledWith({ status: 'approved' });
});

Para componentes com estado interno (como um serviço que gerencia pedidos), utilizamos bancos de dados embutidos (SQLite em memória) ou filas em memória. Isso garante velocidade sem sacrificar a validação de persistência.

3. Testes de Integração com Infraestrutura Real

Testes de integração validam a comunicação entre o serviço e seus sistemas externos. A abordagem moderna utiliza contêineres descartáveis gerenciados por ferramentas como Testcontainers (Java) ou testcontainers-go. Para um serviço que utiliza PostgreSQL e Kafka:

// Teste de integração com PostgreSQL em contêiner (Java + JUnit 5)
@Testcontainers
class OrderRepositoryIntegrationTest {
  @Container
  static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
    .withDatabaseName("testdb")
    .withUsername("test")
    .withPassword("test");

  @Test
  void devePersistirPedido() {
    OrderRepository repo = new OrderRepository(postgres.getJdbcUrl());
    Order order = new Order("123", 250.00);
    repo.save(order);
    assertThat(repo.findById("123")).isPresent();
  }
}

Estratégias para migrações de esquema incluem executar scripts de migração (Flyway, Liquibase) antes de cada bateria de testes e validar a consistência de dados entre serviços por meio de consultas de verificação.

4. Testes de Contrato (Contract Testing)

Testes de contrato garantem que provedores e consumidores de APIs mantenham compatibilidade sem deploy conjunto. O Consumer-Driven Contracts (CDC) é a abordagem mais difundida, implementada por frameworks como Pact e Spring Cloud Contract.

Exemplo de contrato Pact entre um serviço de pedidos (consumidor) e um serviço de estoque (provedor):

// Contrato Pact (formato JSON simplificado)
{
  "consumer": { "name": "OrderService" },
  "provider": { "name": "InventoryService" },
  "interactions": [
    {
      "description": "verificar disponibilidade de produto",
      "request": {
        "method": "GET",
        "path": "/inventory/product/123",
        "headers": { "Accept": "application/json" }
      },
      "response": {
        "status": 200,
        "body": {
          "productId": "123",
          "available": true,
          "quantity": 10
        }
      }
    }
  ]
}

A verificação ocorre em pipelines CI/CD: o provedor executa os contratos do consumidor contra sua API real. Se houver breaking change, o pipeline falha antes do deploy.

5. Testes de Resiliência e Caos

Microsserviços precisam lidar com falhas de forma previsível. Testamos timeouts, retries com backoff exponencial, circuit breakers (Resilience4j, Hystrix) e fallbacks.

Exemplo de teste de circuit breaker com Resilience4j:

// Teste de circuit breaker em modo half-open
@Test
void circuitBreakerDeveAbrirAposFalhas() {
  CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)
    .waitDurationInOpenState(Duration.ofMillis(1000))
    .build();
  CircuitBreaker cb = CircuitBreaker.of("test", config);

  // Simular 3 falhas consecutivas
  for (int i = 0; i < 3; i++) {
    assertThrows(IOException.class, () ->
      cb.executeSupplier(() -> { throw new IOException(); }));
  }

  assertThat(cb.getState()).isEqualTo(CircuitBreaker.State.OPEN);
}

Chaos Engineering leva isso adiante: ferramentas como Chaos Monkey (Netflix) ou Litmus (Kubernetes) injetam falhas controladas em produção ou staging. Validamos degradação graciosa — por exemplo, um serviço de recomendação deve retornar uma lista vazia (não um erro 500) quando o serviço de histórico está indisponível.

6. Testes de Ponta a Ponta (End-to-End)

Testes E2E completos validam fluxos críticos de negócio através de múltiplos serviços. Devem ser focados em happy paths e cenários de alto valor, evitando cobrir cada permutação (o que geraria flakiness).

Estratégias para reduzir flakiness:
- Retries inteligentes com backoff (máx. 3 tentativas)
- Isolamento de dados via namespaces ou bancos de dados dedicados por execução
- Timeouts generosos (30-60 segundos por etapa)

Orquestração em pipelines: deploy paralelo dos serviços em staging, execução dos testes E2E e rollback automatizado em caso de falha. Ferramentas como Cypress, Playwright ou Selenium são comuns para testes de interface, enquanto REST Assured ou Supertest cobrem APIs.

7. Automação e Integração Contínua em Microsserviços

O pipeline de testes deve ser estruturado em estágios progressivos:

1. Testes unitários (rápidos, < 2 min)
2. Testes de componente (com mocks)
3. Testes de integração (com contêineres)
4. Testes de contrato (Pact)
5. Testes E2E (ambiente staging completo)

Estratégias de paralelização: executar testes de integração em paralelo por módulo (cada serviço em seu contêiner). Execução seletiva — apenas serviços modificados executam toda a bateria; serviços não modificados rodam apenas testes unitários.

Métricas de qualidade monitoradas:
- Cobertura de código (mínimo 80% em lógica de negócio)
- Tempo médio de execução da pipeline (alvo < 15 min)
- Taxa de falhas por camada (alvo < 2% em testes não flaky)

Ferramentas como Jenkins, GitLab CI, GitHub Actions ou CircleCI suportam esses padrões com caching inteligente e matrix builds.

Referências