Testes de contrato em microsserviços

1. Fundamentos dos Testes de Contrato

Testes de contrato são uma abordagem de verificação de compatibilidade entre serviços que estabelecem um "acordo formal" sobre como dois serviços devem interagir. Diferentemente de testes de integração tradicionais, que exigem que ambos os serviços estejam em execução simultaneamente, os testes de contrato validam as expectativas de comunicação sem a necessidade de infraestrutura completa.

O propósito central é garantir que um serviço consumidor possa confiar que o serviço provedor atenderá às suas necessidades, sem quebrar acidentalmente integrações existentes. Essa abordagem reduz drasticamente o acoplamento entre equipes e acelera ciclos de desenvolvimento.

Diferenças fundamentais:

Teste de contrato: Verifica se provedor atende às expectativas do consumidor
Teste de integração: Verifica comunicação real entre serviços em execução
Teste end-to-end: Verifica fluxo completo através de múltiplos serviços

Enquanto testes de integração podem ser lentos e frágeis, e testes E2E são caros e demorados, os testes de contrato oferecem feedback rápido e isolado sobre compatibilidade.

2. Abordagens de Teste de Contrato: Consumer-Driven vs. Provider-Driven

Consumer-Driven Contracts (CDC)

Nesta abordagem, o consumidor define suas expectativas e as publica como contrato. O provedor então valida se atende a todas as expectativas registradas. É a abordagem mais comum e é implementada pelo Pact.

Exemplo de contrato CDC (Pact):

{
  "consumer": {
    "name": "ServicoPedidos"
  },
  "provider": {
    "name": "ServicoClientes"
  },
  "interactions": [
    {
      "description": "uma requisição para buscar cliente por ID",
      "request": {
        "method": "GET",
        "path": "/clientes/123",
        "headers": {
          "Accept": "application/json"
        }
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "body": {
          "id": 123,
          "nome": "Maria Silva",
          "email": "maria@exemplo.com"
        }
      }
    }
  ]
}

Provider-Driven Contracts

Aqui o provedor define o contrato e o consumidor valida se consegue consumir a API conforme especificado. Útil quando o provedor possui múltiplos consumidores e precisa garantir estabilidade.

Trade-offs:

Abordagem Vantagens Desvantagens
CDC Consumidor define necessidades reais Pode gerar contratos conflitantes
Provider-Driven Provedor mantém controle Consumidor pode ter necessidades não atendidas

3. Ferramentas e Frameworks Principais

Pact

Ferramenta mais popular para CDC, suporta REST, GraphQL e mensageria. Oferece bibliotecas para múltiplas linguagens.

Exemplo de teste consumidor com Pact (JUnit):

@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "ServicoClientes", port = "8080")
public class ClienteConsumerTest {

    @Pact(consumer = "ServicoPedidos")
    public RequestResponsePact criarPact(PactDslWithProvider builder) {
        return builder
            .given("cliente com ID 123 existe")
            .uponReceiving("uma requisição para buscar cliente")
                .path("/clientes/123")
                .method("GET")
            .willRespondWith()
                .status(200)
                .headers(Map.of("Content-Type", "application/json"))
                .body(new PactDslJsonBody()
                    .integerType("id", 123)
                    .stringType("nome", "Maria Silva")
                    .stringType("email", "maria@exemplo.com"))
            .toPact();
    }

    @Test
    @PactTestFor(pactMethod = "criarPact")
    public void testBuscarCliente(MockServer mockServer) {
        ClienteService clienteService = new ClienteService(mockServer.getUrl());
        Cliente cliente = clienteService.buscarPorId(123);
        assertEquals("Maria Silva", cliente.getNome());
    }
}

Spring Cloud Contract

Integração nativa com ecossistema Spring, suporta contratos definidos em YAML, Groovy ou Java.

Exemplo de contrato Spring Cloud Contract (YAML):

request:
  method: GET
  url: /clientes/123
  headers:
    Accept: application/json
response:
  status: 200
  headers:
    Content-Type: application/json
  body:
    id: 123
    nome: "Maria Silva"
    email: "maria@exemplo.com"
  matchers:
    body:
      - path: "$.id"
        type: by_regex
        value: "\\d+"

Outras Ferramentas

  • PactFlow: Plataforma gerenciada para Pact com publicação e versionamento de contratos
  • Specmatic: Foco em contratos como especificações executáveis baseadas em OpenAPI

4. Ciclo de Vida de um Teste de Contrato

Criação do Contrato

O consumidor define as interações esperadas durante o desenvolvimento, geralmente usando um framework de teste.

Execução no Lado Consumidor

O teste consumidor gera um arquivo de contrato (Pact) que é armazenado localmente ou publicado em um repositório.

Pipeline do consumidor:

1. Desenvolvedor cria teste consumidor
2. Teste gera contrato Pact
3. Contrato é publicado no repositório (Pact Broker)
4. Pipeline do provedor recupera contratos
5. Provedor executa verificações contra contratos
6. Resultado é reportado ao broker

Execução no Lado Provedor

O provedor recupera os contratos e executa verificações para garantir que sua API atende às expectativas.

Exemplo de verificação provedor com Pact:

@Provider("ServicoClientes")
@PactBroker(url = "http://pact-broker:9292")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ClienteProviderTest {

    @LocalServerPort
    int port;

    @BeforeEach
    void setup() {
        System.setProperty("pact.verifier.publishResults", "true");
    }

    @TestTemplate
    @ExtendWith(PactVerificationInvocationContextProvider.class)
    void pactVerificationTestTemplate(PactVerificationContext context) {
        context.verifyInteraction();
    }

    @TestTarget
    public final Target target = new HttpTarget("http", "localhost", port);
}

5. Estratégias de Versionamento e Compatibilidade

Versionamento Semântico de Contratos

Adote versionamento semântico (MAJOR.MINOR.PATCH) para contratos:

  • MAJOR: Mudança incompatível (remoção de campo obrigatório)
  • MINOR: Adição compatível (novo campo opcional)
  • PATCH: Correção sem alteração de interface

Detecção de Quebras

Quando um provedor modifica sua API, os testes de contrato falham automaticamente, sinalizando a quebra. Estratégias de mitigação:

1. Notificar equipe consumidora sobre mudança planejada
2. Implementar versão compatível temporariamente
3. Coordenar deploy simultâneo de consumidor e provedor
4. Usar feature flags para controlar exposição de mudanças

Compatibilidade Retroativa

Sempre que possível, adicione novos campos como opcionais e mantenha campos antigos até que todos os consumidores migrem.

6. Integração com Pipeline de CI/CD

Automação em Pipelines

Configure pipelines para executar testes de contrato automaticamente:

# Exemplo de pipeline GitLab CI
stages:
  - test
  - verify-contracts
  - deploy

verify-contracts:
  stage: verify-contracts
  script:
    - ./gradlew pactVerify
  only:
    - main
    - develop

Verificação Pré-Deploy

Antes de promover um deploy para produção, verifique a compatibilidade com todos os consumidores registrados:

1. Pipeline do provedor executa verificações de contrato
2. Se falhar, bloqueia deploy (quebra de build)
3. Se passar, permite deploy para ambiente de staging
4. Após validação em staging, promove para produção
5. Publica resultado da verificação no Pact Broker

Estratégias de Deploy Seguro

  • Canary: Libere nova versão para subconjunto de tráfego
  • Blue-Green: Mantenha versão antiga como fallback
  • Feature Flags: Desative mudanças que quebram contratos

7. Desafios e Boas Práticas

Gerenciamento de Múltiplos Consumidores

Quando vários consumidores têm contratos conflitantes, estabeleça um processo de governança:

1. Reunião periódica de alinhamento de contratos
2. Priorização baseada em impacto de negócio
3. Versões alternativas da API (v1, v2)
4. Depreciação gradual de endpoints antigos

Testes em Ambientes Assíncronos

Para mensageria e eventos, use contratos baseados em mensagens:

Exemplo de contrato de mensageria com Pact:

{
  "description": "uma mensagem de pedido criado",
  "providerStates": [
    {
      "name": "um pedido com ID 456 foi criado"
    }
  ],
  "contents": {
    "id": 456,
    "status": "CRIADO",
    "total": 150.00
  },
  "messageType": "PedidoCriado"
}

Monitoramento Contínuo

Implemente monitoramento para detectar quebras em produção:

  • Pact Broker: Dashboard com status de verificações
  • Alertas: Notificações quando contrato quebra
  • Métricas: Taxa de sucesso de verificações ao longo do tempo

Boas Práticas Essenciais

  1. Mantenha contratos simples: Foco em interações reais, não em todos os cenários possíveis
  2. Versionamento explícito: Use tags e versões no Pact Broker
  3. Testes em isolamento: Não dependa de outros serviços durante testes de contrato
  4. Documentação viva: Contratos servem como documentação executável
  5. Feedback rápido: Execute verificações em cada commit

Referências