22 de outubro de 2019
No desenvolvimento de micro-serviços, por exemplo, é importante a existência de uma cultura de princípios guiando o processo de desenvolvimento. Um destes princípios é a descentralização: todos os times deve possuir controle e liberdade sobre seus serviços (desde o provisionamento de recursos ao deploy independente). Um ponto importante para garantir descentralização é a coordenação entre serviços, que devem ser o mais desacoplados possível. Um serviço deve responder de forma consistente para que outros serviços possam utilizá-lo apropriadamente, mas em ambientes de constantes modificações esta coordenação entre equipes pode ser falível e demandar testes de integração complexos de e2e (end-to-end).
Como é possível ter a segurança de que serviços inter-dependentes não sejam negativamente impactados com mudanças? Esta é a proposta dos testes baseados em contratos com consumidores (ou Consumer-Driven Contracts). Este padrão garante a consistência nos relacionamentos entre aplicações ao efetuar testes nas APIs impondo a conformidade com um contrato que é estabelecido pelas expectativas das aplicações que consomem as APIs.
Logo, temos dois tipos de aplicações envolvidas:
A relação entre estas aplicações é definida por uma forma de contrato, um documento que especifíca as interações existentes: quais requisições o consumidor faz e quais respostas são esperadas do provedor para essas requisições. Ou seja, o esquema do contrato é definido com base em exemplos positivos.
O consumidor define como espera ser respondido (momento de criação do contrato.) Quando são feitas alterações no código do serviço provedor, o teste de validação informa se o contrato foi quebrado em decorrência da alteração, interrompendo o progresso da build na pipeline de deploy, por exemplo.
Esta prática possui benefícios práticos:
Certamente um dos maiores benefícios é o desacoplamento entre as aplicações uma vez que as interfaces são mais bem definidas.
Neste artigo irei definir esta prática nos termos do framework Pact, uma ferramenta de contract tests; provavelmente a mais utilizada para este fim. O ecossistema de Pact inclui também o Pact Broker, aplicação para compartilhamento de contratos e resultados de verificações.
Estarei utilizando uma aplicação desenvolvida por mim com base no exemplo do pact-workshop[^repo-pact-workshop] em Javascript, mas Pact possui implementações e guias para diferentes linguagens.
Pact atua como mock nos dois lados da interação: para o consumer ele faz o papel do provider e o contrário para o provider. O ponto de partida é a especificação no lado consumer. O Pact intercepta as requisições que iriam para o provider respondendo com o que o consumer define no Pact test como esperado, neste momento os testes unitários do consumer também são realizados para garantir que com a resposta esperada do provider ele saberá operar como esperado também.
Ao final dos testes no lado consumer, se todos os teste passam, é gerado um arquivo .json
com um conjunto de interações (especificações de requisição e resposta esperada). Este arquivo é, por assim dizer, o pacto (contrato) do relacionamento, o provider precisará somente dele para validar se está de acordo com as especificações do consumer. O pacto deve ser distribuído para que o provider tenha acesso. Existem várias formas de fazer esta distribuição: transferir via diretório compartilhado, repositório git ou, o mais recomendado, um Pact Broker (que é abordado na sessão Pact Broker).
A figura abaixo dá uma visão geral do funcionamento do Pact.
A aplicação exemplo no lado consumer passa no corpo da requisição uma lista de itens (descrições, preços e quantidades) para o provider que, então, calcula o valor total de uma compra com tais itens e define uma porcentagem máxima de desconto aplicável. O consumer adiciona uma restrição extra na porcentagem de desconto limitando-o a 15%.
1 | const checkoutOrder = items => { |
No lado do provedor o tratamento para a rota de checkout é calcular o valor total da ordem fazendo uma redução no array de itens recebido.
1 | server.post('/checkout', (req, res) => { |
O teste está definido utilizando o framework Mocha com Chai. O Pact entra inicialmente como mock do serviço provider. É feita a importação e a definição do Pact em que são especificados nomes, portas e arquivos em que serão realizados outputs de logs e do pacto.
1 | const Pact = require('@pact-foundation/pact').Pact; |
No corpo do teste esta definida a interação “a request for total amount” que define o formato da requisição (withRequest
) e a resposta que o mock criado pelo Pact irá dar ao consumer (willRespondWith
). Em seguida é feita a validação da resposta, verificando se o totalAmount
é igual ao valor esperado (156.5) e se o valor final com desconto calculado pelo consumer é também o esperado (133.025).
As validações são feitas sobre os resultados provenientes do consumer pois é justamente o que se deseja testar, se o consumer consegue prover suas funcionalidades adequadamente com os retornos consistentes vindos do provider.
1 | describe('and passing a valid items set', ()=> { |
Quando finalizada a execução dos testes, o Pact cria, no diretório definido anteriormente, o arquivo com as especificações. Este arquivo de pacto deve ser distribuído para o provider. Este projeto utiliza Pactflow, ferramenta online de Pact Broker para distribuição de pactos. É possível, também, executar Pact Broker em um servidor próprio[^pact-broker].
1 | { |
O lado provider tem a responsabilidade de estar em conformidade com o contrato. O teste consiste em buscar a distribuição do arquivo de pacto .json
e validar as interações no ponto de vista do provider. A especificação, portanto, deve inicializar uma instância do servidor provider e utilizar o Verifier
do Pact em seguida.
Neste projeto, como está sendo utilizado Pactflow, as configurações incluem pactBrokerUrl
e o pactBrokerToken
que são as informações de conexão ao broker. Mas é possível configurar para utilizar o arquivo .json
informando o caminho para o arquivo localmente.
1 | describe('Provider test', () => { |
Executando os testes é possível saber se, em produção, a aplicação entrará ou não em conflito com o consumer. Isto possibilita uma grande liberdade na alteração de como as coisas são feitas internamente e mesmo como os retornos são realizados. Imagine que a função de checkout foi alterada e, por algum engano, o retorno acrescente 5 reais ao montante final. A execução do teste reclamaria que esperava um valor diferente.
1 | Diff |
Grande parte do conteúdo deste artigo se baseia conceitualmente na publicação em vídeo “The Principles of Microservices” de O’Reilly Media apresentado por Sam Newman e no livro “Building Microservices” também do Newman (uma leitura recomendada).
Imagens e conceitos do framework Pact são do Pact.
[^repo-pact-workshop]: Repositório no Github: pact-workshop-js
[^pact-broker]: Página do Pacto Broker
Este trabalho está licenciado com uma Licença Creative Commons - Atribuição-NãoComercial 4.0 Internacional.