Bem-vindo ao maravilhoso mundo dos testes de contrato

Publicado por Bruno Tanoue no dia qa

Blue Bug

O desenvolvimento de software sempre modificou-se dinamicamente ao decorrer do tempo. Novos paradigmas de programação foram criados, novos tipos de linguagem, metodologias e processos. A arquitetura dos sistemas também não poderia ficar fora, começando com uma arquitetura monolítica, passando para uma arquitetura orientada a serviços (SOA), até a arquitetura com maior destaque no momento: microservices.

O aumento da utilização das arquiteturas baseadas em services e microservices fez com que um grave problema entrasse em foco: a quebra de contrato entre o provedor do serviço e o cliente.

Na construção de um sistema, existem duas vertentes para a evolução da arquitetura de microservices.

A primeira vertente prega que sistemas novos não sejam construídos diretamente nesta arquitetura, mas que o sistema seja quebrado em pequenos serviços, conforme o monolito se torne grande demais (o que atualmente é o nosso caso). Este padrão é descrito por Martin Fowler, no artigo Monolith First.

A corrente contrária defende que os sistemas sejam projetados em microserviços desde o início, conforme explica o artigo Don’t start with a monolith.

As questões a se ponderar inicialmente na construção de um novo sistema são as seguintes: vale a pena adicionar complexidade e criar um sistema pequeno com microservices? E se sistema já inicialmente for planejado para ter um pequeno tamanho até o final do seu ciclo de vida?

Monolith vs Microservice Monolito vs Microservice

A criação e a evolução do RDStation

O RDStation, nosso principal produto na Resultados Digitais nasceu como um monolito (como na grande maioria dos casos), principalmente pela urgência da entrega de valor em detrimento das curvas de aprendizado em começar diretamente em uma arquitetura de microservices (o que era algo relativamente novo na época). Ter um produto rapidamente provando valor é uma aposta bem mais certeira do que um sistema estruturado nas tendências atuais e a aposta em microservices poderia ter colocado em risco o futuro do produto. (entrar em um mercado já saturado pode algumas vezes significar o fim, mesmo antes do lançamento).

Atualmente, o sistema se tornou robusto demais, com milhares de linhas de códigos e dependências. Alguns processos se tornaram lentos por causa disso, como o deploy, o build e a revisão de PR’s, travando um pouco da nossa metodologia Ágil.

Em paralelo ao crescimento deste monolito, o RDStation caracteriza-se por ser uma plataforma de marketing digital completa. Para atendermos a necessidade de demanda de todos os clientes e desenvolvermos nossas features, primeiramente procuramos por alguma coisa disponível no mercado (ou seja, API’s de terceiros) para nos auxiliar no desenvolvimento. Na maioria dos casos encontramos algumas API’s disponíveis para o uso e, consequentemente, aumentamos consideravelmente a quantidade de API’s de terceiros conectadas no nosso sistema. Se não houver nenhuma outra opção, desenvolvemos a partir do zero.

Trabalho em uma equipe que é responsável pela feature de postagens em mídias sociais e como realizamos postagens em três redes sociais diferentes (Facebook, Twitter e LinkedIn), temos contato diário com três API’s de terceiros, nos preocupando sempre com a quantidade de requisições que estamos efetuando, o número de erros nessas comunicações e também ficamos de olho se alguma modificação foi feita do outro lado. Cada uma delas utiliza um padrão diferente de atualização de API’s, englobando desde o aumento de versão de API’s até a modificação de dados na própria versão da API corrente.

APIs examples

Um grande novo problema: Inconsistência de contratos

Baseado nestes fatos, a necessidade da mudança de arquitetura dos sistemas para microservices, além da comunicação com API’s externas fez com que um novo tipo de problema ficasse em evidência: a quebra de contrato entre o provedor e o cliente. Os testes de contrato nesse contexto auxiliam para que não haja um ruído sobre os dados que trafegam em ambos os lados.

Uma mudança de estrutura ou tipo de dado pelo provedor de serviço pode causar grandes perdas para o cliente, se o cliente não for comunicado sobre a mudança. Mesmo que essa comunicação tenha sido feita, é possível que o cliente não saiba o real impacto, apesar de possuir tipos de testes focados nas comunicações. Uma mudança do lado do cliente também pode fazer com que os dados retornem em uma maneira diferente do esperado.

Possíveis abordagens para contornar os problemas de contrato

Os testes de integração, por exemplo, podem até resolver estes problemas, mas pensando em um cenário de testes da stack inteira desde o cliente até o provedor de serviço, podem se tornar demorados e levar uma quantidade razoável de tempo, fazendo com que o impacto da modificação ainda seja grande. Imagine a variável de latência de rede para uma API (ou um banco de dados utilizado) que é localizada em uma outra extremidade do planeta, ou ainda a concorrência da rede de milhares de clientes por um determinado serviço.

Integration Test Full-Stack

Por outro lado se essa comunicação estiver apenas mockada, pode ocasionar um falso positivo, o que pode ser mais grave ainda, ocasionando uma falta de confiança sobre a suíte de testes.

Os testes de contrato em um contexto de services e microservices (ou testes de contrato de integração) são efetivos, já que comparam principalmente os tipos de dados da comunicação dos endpoints de cliente e provedor com um arquivo de contrato, não se importando com o que ocorre antes e depois disso. Se um teste de contrato for quebrado, quer dizer que houve uma mudança do lado do provedor, ou as modificações feitas pelo cliente tem um resultado diferente do que era esperado anteriormente.

O que são contratos?

Os contratos são a base de comparação dos testes de contrato de integração. Em forma de arquivos (json, xml, yaml, etc), eles contém dados de requisição, como headers, url destino, protocolo HTTP utilizado e parâmetros de envio, além de dados de retorno, como headers e código HTTP do retorno. Também possuem alguns exemplos de dados e tipagem de todos os dados de resposta. Para o nosso caso, a última informação citada é a mais importante para verificar se houve a quebra de contrato de algum dado recebido.

Contract Example

O que são testes de contrato?

Em um contexto de teste unitários, os testes de contrato descrevem a interface de programação disponível em um objeto, ou seja, são verificados, por exemplo, se os parâmetros e o retorno de um método mockado tem a mesma tipagem dos parâmetros e retorno do método original. Além disso, também garantem que o objeto original possua os métodos que estão sendo simulados (com mocks) em algum teste. Este não é o tema principal deste post, mas se você deseja se aprofundar mais sobre este tipo de teste de contrato, recomendo ler o post Usando Testes de Contrato e Colaboração.

Em um contexto de services e microservices, os testes de contrato verificam a validade dos mocks que representam a simulação das comunicações entre serviço e consumidor. Com o decorrer do tempo, podem existir casos em que o provedor precise fazer modificações no seu serviço, seja alterando, adicionado ou apagando dados e dependendo do processo de atualização de API que ele utilize (se não utilizar um padrão de aumentar a versão de API para as alterações por exemplo), os mocks podem ficar obsoletos, causando falsos positivos e fazendo com que grandes incidentes sejam causados em produção.

O funcionamento deste tipo de teste é bem simples. O consumidor faz uma chamada para o serviço e recebe os dados de retorno. A tipagem desses dados é comparada com com a tipagem dos dados do mock (através de um arquivo de contrato) e se houver alguma inconsistência nessa comparação, o teste irá falhar, emitindo um aviso de que a API está retornando dados diferentes do que o consumidor está esperando.

Integration Contract Test Diagram

Existe uma recomendação também para que estes testes sejam implementados em uma suíte separada do build regular, por dois motivos principais: utilização de requisições reais e frequência de mudança de API. A utilização de requisições reais em um build regular é muito perigosa, pois pode impedir a entrega de features por causa de testes falhando por alguma instabilidade da API (ou se ela estiver fora do ar). A frequência de mudança de API é diferente da frequência de mudança de código, ou seja, esses testes vão ser executados excessivamente e dificilmente irão falhar como falham os testes tradicionais da pirâmide de testes.

Conclusão

A evolução dos sistemas para microservices resultou com que novos problemas entrassem em foco, fazendo com que métodos alternativos de testes ganhassem mais destaque além dos testes tradicionais da pirâmide de testes. Apesar de muitas empresas terem experiências ruins na migração para este tipo de arquitetura e dos riscos de migração, existem vantagens bem relevantes em relação a aderir ao uso deste tipo de tecnologia, como builds e entregas mais rápidas e modularidade das API’s.

Este é um post introdutório relacionado a uma série de posts que terão como foco principal os testes de contrato de integração. A aplicação deste tipo de teste pode ser muito ampla e variada, dependendo das características das API’s utilizadas. Eles também podem auxiliar na construção e evolução de microserviços, mitigando os problemas de alterações e versionamento. Também auxiliam para que o processo ocorra de maneira mais suave e controlada.

E você, qual a sua opinião sobre microservices? Já utilizou os testes de contrato neste contexto alguma vez?

Leia mais sobre:

Bruno Tanoue

Bruno Tanoue

Quality Assurance Engineer

Comentários