Testes de contrato – O que, quando e por quê?

Neste artigo você vai ver:

Existem vários tipos de testes em aplicações, com diferentes objetivos e recursos. Por isso, hoje queremos te apresentar o que são testes de contrato, além de quando e porquê usá-los.

Além disso, neste artigo você vai ver uma introdução sobre o PACT Framework e um teste prático de testes de contrato em Java + Spring.

O desafio que nos faz adotar testes de contrato e o PACT Framework

Muito se discute sobre a integração de serviços em arquiteturas mais inovadoras, como a de microsserviços e outras.

A arquitetura de microsserviços, em especial, proporciona a separação do sistema em componentes ou processos independentes que possuem comunicação entre si e trabalham juntos para o mesmo fim.

Podemos imaginar a comunicação entre dois componentes distintos, fazendo uma pequena analogia a um quebra-cabeças. Uma peça precisa que seu par possua o encaixe perfeito ao qual ela está preparada para se juntar. Da mesma forma, um sistema que consome (consumer) as informações de outro sistema (provider), precisa que as informações disponibilizadas por este outro, atendam à sua necessidade.

Se adotarmos um pensamento simplista podemos não enxergar o quão complexas podem vir a se tornar estas comunicações, visto que, uma peça do quebra-cabeças possui encaixes com outras peças e, estas outras, com outras. Acontece de forma parecida com os serviços em uma arquitetura de microsserviços, criando um ambiente sistemático repleto de interações entre processos e com alta complexidade.

Já imaginou o tamanho do impacto causado por uma falha na comunicação entre algum destes serviços? Quantos outros também poderão falhar? Qual o impacto no produto?

E como podemos mitigar isso? 

E sim, eu disse mitigar! Pois nenhuma solução é “bala de prata” e resolve 100% dos problemas!

Mas e agora? 

Em tempos de grande utilização deste tipo de arquitetura algumas perguntas vêm à mente, por exemplo: 

  • Como garantir que as aplicações, em produção, não quebrem por problemas nesta comunicação? 
  • Como realizar testes nesta comunicação?
  • mockar é suficiente?
  • Testes integrados previnem mudanças no contrato?

Utilizando a abordagem CDC (Consumer-Driven Contracts)

Essas e outras questões podem ser respondidas através da abordagem CDC (Consumer-Driven Contracts).

Em suma, essa abordagem define que o consumer, sistema que consome a informação, implemente, através de um contrato, as regras que exprimem suas necessidades, suas expectativas.

Já o provider, sistema que provê a informação, faz a validação se está, de fato, fornecendo o que lhe é pedido.

Essa abordagem também garante que os dois lados da comunicação façam a validação dos termos deste contrato, de uma forma simples e rápida, utilizando-se de testes de unidade.

Mas como? 

Há muitas maneiras e ferramentas. Desde a utilização do Postman, até frameworks específicos. Hoje abordaremos a utilização do Pact Framework.

Mas o que são testes de contrato?

Antes de tudo precisamos entender o que são testes de contrato realmente e por que são importantes.

Um contrato, na vida real, nada mais é do que um acordo formalizado entre duas partes. Isso ocorre da mesma forma com os sistemas, estabelecendo-se regras de comunicação que devem ser seguidas pelos dois lados.

Este contrato garante que, se suas regras forem seguidas, toda comunicação entre as partes ocorrerá conforme o esperado e, sabendo disso, já é possível perceber a importância em garantir tais regras e o tamanho do problema que poderá acontecer se estas regras não forem seguidas.

Daí surgem os testes de contrato.

Visando garantir o bom funcionamento desta comunicação, precisamos validar se tudo o que é esperado por um sistema consumidor é fornecido por um sistema provedor. 

É preciso ter certeza de que o que se espera é o que chega e também se o que sai é, de fato, o que se espera.

Para isso construímos testes dos dois lados da comunicação. No consumer para estabelecer as regras e verificar se elas são suficientes para o atendimento de suas necessidades. No provider para validar se o sistema entrega o que é estabelecido no contrato com o consumer.

Pact Framework 

O Pact Framework realiza a criação e os testes dos contratos entre as partes, não limitando-se à uma linguagem de programação, pois a linguagem nada interfere nas regras de comunicação entre os sistemas.

Na própria documentação do Pact é possível encontrar vários exemplos de implementações de pequenos sistemas utilizando linguagens diferentes, validando seus respectivos contratos.

Testes de contrato e Pact Framework em ação: teste prático!

Para fornecer o exemplo mais simples possível e poder focar sua atenção no real propósito deste artigo, que é um melhor entendimento sobre testes de contrato, criei duas aplicações REST, com Java+Spring, que possuem comunicação entre si, uma denominada “pact_consumer” e outra “pact_provider”.  

O “pact_consumer” expõe três endpoints na porta 8081, um método POST para criar um usuário (“/users”), um método GET que retorna a lista dos usuários cadastrados (“/users”) e, por fim, outro método GET que busca um usuário por seu respectivo id (“/users/{id}”).

Já o “pact_provider” recebe tais requisições e realiza a comunicação com a base de dados, retornando o que foi solicitado pelo “pact_consumer”.

As aplicações podem ser visualizadas no seguinte repositório.

Nelas realizei os respectivos testes de contrato para estes métodos e a integração com o Pact Broker para tornar o processo de gerenciamento do contrato mais automatizado.

Se achar interessante abra estas aplicações localmente, desta maneira será possível acompanhar as ideias aqui abordadas e visualizá-las na prática.

Fluxo de atuação do Pact Framework

Agora, voltando as atenções para o Pact Framework, precisamos entender como este agente atua na criação e validação do contrato.

Na Figura 1, podemos observar que, para esta abordagem de testes, a comunicação entre os dois sistemas não ocorre. O que ocorre é a definição das expectativas no contrato pelo consumer e a validação deste contrato pelo provider.

Figura 1: Desenho esquemático da utilização do Pact como interface para os contratos. Acima temos o primeiro título “Step 1 - Define Consumer expectations”. Abaixo dele temos um desenho com dois quadrados de mesmo tamanho, o da esquerda possui a palavra Consumer em seu interior, o da direita possui a palavra Provider em seu interior. Entre eles uma linha na vertical com a palavra pact no topo. Entre o quadrado da esquerda e esta linha temos duas setas na horizontal e em direções opostas, a primeira, localizada acima da outra, segue para a direita e é denominada http request, já a segunda segue para a esquerda e é denominada http response. Abaixo deste desenho temos o segundo título “Step 2 - Verify expectations on Provider”.  Abaixo dele temos um desenho com dois quadrados de mesmo tamanho, o da esquerda possui a palavra Consumer em seu interior, o da direita possui a palavra Provider em seu interior. Entre eles uma linha na vertical com a palavra pact no topo. Entre o quadrado da direita e esta  linha temos duas setas na horizontal e em direções opostas, a primeira, localizada acima da outra, segue para a direita e é denominada http request, já a segunda segue para a esquerda e é denominada http response.
Fonte: Pact-foundation

Pact Broker: conceito e pontos de atenção

Para tornar esse processo de publicação do contrato automatizado, podemos utilizar o Pact Broker para receber o contrato publicado pelo consumer e disponibilizá-lo para o provider, bem como verificar ambas as validações.

Pact Broker é a ferramenta responsável pelo gerenciamento dos contratos criados através do Pact. Possui uma interface amigável e possibilita um fácil controle dos processos.

Por serem testes de unidade, os testes de contrato são consideravelmente mais rápidos que testes integrados. Outro ponto importante é que não há a necessidade de subir outros sistemas para validar sua comunicação. 

Mas atenção!

Testes integrados são muito importantes e devem caminhar junto com testes de contrato, ambos se complementam e devem ser usados, criteriosamente, cada um à sua necessidade.

Pact Broker no nosso teste

O Pact Broker recebe a publicação de todos os contratos estabelecidos pelo consumer e realiza seu versionamento e sua apresentação de forma resumida em sua tela principal. Os detalhes de cada contrato podem ser visualizados ao clicarmos no ícone do contrato disponível em cada publicação.

Abaixo podemos ver um resumo da publicação do contrato, pelo consumer do sistema de exemplo deste artigo, no Pact Broker.

Figura 2: Trecho da tela aberta no navegador pelo Pact Broker. No canto superior esquerdo o título “Pacts”. Logo abaixo um campo de pesquisa com dois botões, um “Submit” e o outro “Reset”. Mais abaixo, uma tabela com duas linhas, a primeira com os títulos e a segunda com os valores do contrato publicado. Seguindo a descrição da tabela, da esquerda para a direita, temos: título: Consumer, valor: Consumer, título: Provider, valor: Provider, título: Latest pact published, valor: 4 minutes ago, título: webhook status, valor: Create, título: Last verified, valor: (nesta célula não há nada escrito).
Fonte: O autor.

Neste momento já temos as regras de comunicação do consumer no Pact Broker, mas ainda não sabemos se o  provider está alinhado com tais regras. Precisamos realizar a verificação deste contrato no provider e, se tudo estiver ok, publicar a verificação de volta ao Pact Broker. Para isto, rodamos os testes do respectivo contrato no provider e, após passarem, realizamos a publicação do resultado no Pact Broker. A cor verde simboliza que o provider está atendendo a todas as regras estabelecidas no contrato pelo consumer, conforme Figura 3. 

Figura 3: Trecho da tela aberta no navegador pelo Pact Broker. No canto superior esquerdo o título “Pacts”. Logo abaixo um campo de pesquisa com dois botões, um “Submit” e o outro “Reset”. Mais abaixo, uma tabela com duas linhas, a primeira com os títulos e a segunda com os valores do contrato publicado. Seguindo a descrição da tabela, da esquerda para a direita, temos: título: Consumer, valor: Consumer, título: Provider, valor: Provider, título: Latest pact published, valor: 10 minutes ago, título: webhook status, valor: Create, título: Last verified, valor: 1 minute ago (esta célula está preenchida com a cor verde).
Fonte: O autor

Este processo pode tornar-se ainda mais automatizado quando integramos em uma pipeline de CI, onde, a cada entrega, em qualquer dos lados, os testes serão executados e a verificação poderá ser publicada automaticamente, garantindo que o contrato sempre esteja válido durante o ciclo de vida do desenvolvimento do sistema.

Como testar?

Independentemente da linguagem de programação utilizada, a abordagem do Pact será a mesma. A imagem abaixo apresenta o fluxo de testes e validação dos contratos.

Figura 4: Desenho esquemático do fluxo de testes e validação de contratos. Possui dois cubos acima simbolizando dois sistemas diferentes, o cubo da esquerda é denominado Service A - Consumer, já o da direita é denominado Service B - Provider. Entre eles há um símbolo que representa a sua comunicação denominado de Interface (e.g. REST). Abaixo dos dois cubos há um cubo de tamanho menor denominado de Executable Tests. Entre o  cubo da esquerda (Service A - Consumer) e o  cubo de baixo (Executable Tests), há uma seta tracejada no sentido do cubo da esquerda (Service A - Consumer) para o cubo de baixo (Executable Tests) denominada de write & publish. Já entre o  cubo de baixo (Executable Tests) e o cubo da direita (Service B - Provider), há uma seta tracejada no sentido do cubo de  baixo (Executable Tests) para  o cubo da direita (Service B - Provider) denominada fetch & execute.
Fonte: The practical test pyramid

O consumer escreve seus testes de unidade definindo suas expectativas de retorno através de mocks do provider. Até este ponto pouca coisa muda, exceto a sintaxe, com relação à abordagem tradicional de mockar um cliente externo. Definimos o que queremos validar e escrevemos os testes para as respostas do mock e, se os testes passarem, o Pact gera um contrato em formato Json com tais regras.

No exemplo abaixo vemos o estabelecimento das regras que serão criadas, pelo consumer, no contrato entre ele e o provider (sistemas de exemplo).

@Pact(consumer = consumerName)
public RequestResponsePact postSingleUser(PactDslWithProvider builder) {

 PactDslJsonBody bodyResponse = new PactDslJsonBody()
             .integerType("id", 1L)
          .stringType("message", "Successfully registered user!");
return builder
       .given("user does not exists")
       .uponReceiving("a POST request to create a user")
       .path("/users")
       .method(HttpMethod.POST.name())
       .body(responseBody, ContentType.APPLICATION_JSON)
       .willRespondWith()
       .headers(headersContentType)
       .matchHeader("Location", ".*/users/[0-9]+", "http://localhost:8080/users/1")
       .status(201)
       .body(bodyResponse)
       .toPact();
}

Nota-se que este mock está preparado para receber uma requisição do tipo POST, contendo informações para cadastrar um usuário e deverá responder com um header contendo um content type do tipo Json, um header Location, o status code 201-Created e um body contendo um “id” em formato numérico e uma “message” em formato de String.

Agora, através de um teste simples, conforme exemplo abaixo, enviamos esta requisição e validamos o retorno. Desta maneira, após o teste ser bem sucedido, o Pact cria o contrato contendo todos estes retornos esperados, a fim de validá-los no provider posteriormente.

@Test
@DisplayName("Should create a user")
@PactTestFor(pactMethod = "postSingleUser")
void testPostSingleUser(MockServer mockServer) throws IOException {
    HttpResponse httpResponse = Request.Post(mockServer.getUrl() + "/users")
            .bodyString(responseBody, ContentType.APPLICATION_JSON)
            .execute().returnResponse();
    assertThat(httpResponse.getStatusLine().getStatusCode(), is(equalTo(201)));
}

Partindo para o provider, devemos garantir que tudo ocorra da forma esperada ao verificarmos o contrato elaborado pelo consumer. O contrato trará as informações das requisições a serem enviadas e o provider fará a verificação das respostas que produz, com as respostas esperadas pelo contrato. Dessa forma se algo estiver sendo retornado diferente do esperado, a validação falha e os testes quebram. No próximo exemplo, vemos o código da verificação no provider.

@TestTemplate
@ExtendWith(PactVerificationSpringProvider.class)
@DisplayName("Should check the contract with the consumer")
void pactVerificationTestTemplate(PactVerificationContext context) {
    context.verifyInteraction();
}
@State(value = "user does not exists", action = StateChangeAction.SETUP)
void createUser() {
    userRepository.deleteAll();
}

A anotação @State determina o estado que a aplicação deve se encontrar para que o teste possa obter sucesso. Neste cenário a base de dados deve estar vazia e, caso não esteja, utilizei o método deleteAll() para esvaziá-la. Lembrando que o banco de dados no contexto dos testes, aqui neste exemplo, é um banco em memória que se difere do banco de produção. 

Desta maneira estamos validando ambas as comunicações sem a necessidade dos sistemas de fato se comunicarem. 

Estes testes tornam-se garantidores das regras do contrato e a cada nova atualização do sistema eles avaliam a integridade do contrato.

Mas afinal, o que validar?

Essa pergunta é muito pertinente no contexto dos testes de contratos, pois o foco aqui não é o dado retornado em si, mas sim as regras estabelecidas no contrato.

No exemplo que estamos utilizando, ao enviarmos uma requisição através do consumer, para o provider, o que o contrato deveria conter para garantir a boa e imutável comunicação entre as partes?

Validar o retorno do “id” e de uma mensagem de sucesso é suficiente?

Não! Isso poderá ser validado em testes específicos para este fim. Vai muito além disso!

Precisamos aqui garantir que as regras deste contrato amarrem a comunicação entre as aplicações, de tal forma que uma quebra desse contrato acenda um alerta e impeça que o código vá para produção, por exemplo.

Então o que buscamos validar nos contratos?

Buscamos validar regras que dificilmente irão ser alteradas com a evolução do produto. Como o retorno de um status code 201 ao criarmos um registro, por exemplo. É algo que, após definido no escopo do projeto, muito provavelmente se torne uma regra de comunicação.

Validar o formato da resposta também é algo importante. Se é Json, XML, ou outro formato, pois evita que um dos lados forneça informações em um formato que o outro não esteja preparado para trabalhar.

Validar as autorizações ou permissões.

Validar a tipagem dos dados retornados também é outro bom exemplo. Isso, o tipo e não o conteúdo, deixemos o conteúdo novamente para os testes integrados. Podemos validar se ao esperar uma String estamos, de fato, recebendo uma String.

Essas são algumas das regras que constroem o contrato entre as partes e definem a correta comunicação entre elas. Quanto mais bem elaborado o contrato, mais robusta e confiável torna-se esta comunicação. 

Conclusão 

Realizar testes nos contratos é mais uma maneira de elevar os padrões de qualidade dos produtos de software que entregamos. A busca por qualidade deve ser constante e devemos defender práticas que elevem esses níveis.  

É importante ressaltar que essa abordagem não substitui outros tipos de testes, mas sim os complementa.

Quanto mais buscarmos a qualidade nos nossos produtos, maior é o seu impacto positivo e maior é o valor que entregamos.

Referências

Capa do artigo Testes de contrato em que vemos uma mesa do espaço de trabalho com smartphone, óculos, caderno azul, xícara de café, teclado.
Foto de Charles Aparecido Rodrigues
QA Automatizador
QA e engenheiro civil, apaixonado por natureza, viagens e tecnologia.

Este site utiliza cookies para proporcionar uma experiência de navegação melhor. Consulte nossa Política de Privacidade.