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?
- Só 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.
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.
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.
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.
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
- Consumer-Driven Contracts: A Service Evolution Pattern – Ian Robinson
- The Practical Test Pyramid – Ham Vocke
- Pact-specification – Pact-foundation
- Pact JVM – Pact
- Exempos Pact Framework – Charles Rodrigues
- Testes de contratos com PACT #1 Conceitos – Zup
- Testes de contratos com PACT #2 – Consumer Driven Contract – Zup
- Testes de contratos com PACT #3 – Hands-on – Zup