Você vai responder por tudo! Contratos orientados ao consumidor através dos olhos do desenvolvedor

Neste artigo, falaremos sobre os problemas que o Consumer Driven Contracts resolve e mostraremos como aplicá-lo usando o exemplo do Pact com Node.js. e Spring Boot. E fale sobre as limitações dessa abordagem.


Edição


Ao testar produtos, geralmente são usados ​​testes de cenário nos quais a integração de vários componentes do sistema em um ambiente especialmente selecionado é verificada. Tais testes em serviços ao vivo fornecem o resultado mais confiável (sem contar os testes em batalha). Mas, ao mesmo tempo, eles são um dos mais caros.

  • Muitas vezes, acredita-se erroneamente que o ambiente de integração não deve ser tolerante a falhas. SLA, as garantias para esses ambientes raramente são expressas, mas se não estiver disponível, as equipes precisam adiar lançamentos ou esperar o melhor e entrar em batalha sem testes. Embora todos saibam que a esperança não é uma estratégia . E novas tecnologias de infraestrutura complicam apenas o trabalho com ambientes de integração.
  • Outra dor é trabalhar com dados de teste . Muitos cenários exigem um certo estado do sistema, equipamentos. Quão perto eles devem estar do combate aos dados? Como atualizá-los antes do teste e limpá-lo após a conclusão?
  • Os testes são muito instáveis . E não apenas pela infraestrutura mencionada no primeiro parágrafo. O teste pode falhar porque uma equipe vizinha lançou suas próprias verificações que quebraram o estado esperado do sistema! Muitas verificações falsas negativas e testes @Ignored terminam a vida em @Ignored . Além disso, diferentes partes da integração podem ser suportadas por diferentes equipes. Eles lançaram um novo candidato a lançamento com erros - quebraram todos os consumidores. Alguém resolve esse problema com loops de teste dedicados. Mas ao custo de multiplicar o custo do suporte.
  • Tais testes levam muito tempo . Mesmo com a automação em mente, os resultados podem ser esperados por horas.
  • E ainda por cima, se o teste realmente deu certo, está longe de ser sempre possível encontrar imediatamente a causa do problema. Pode se esconder profundamente atrás das camadas de integração. Ou pode ser o resultado de uma combinação inesperada de estados de muitos componentes do sistema.

Testes estáveis ​​em um ambiente de integração exigem um investimento sério de controle de qualidade, desenvolvimento e até operações. Não é de admirar que eles estejam no topo da pirâmide de teste . Esses testes são úteis, mas a economia de recursos não permite que eles verifiquem tudo. A principal fonte de seu valor é o meio ambiente.

Abaixo da mesma pirâmide, há outros testes nos quais trocamos confiança por dores de cabeça de suporte menores - usando verificações de isolamento. Quanto granular, quanto menor a escala do teste, menor a dependência do ambiente externo. No fundo da pirâmide existem testes de unidade. Verificamos funções individuais, classes, operamos tanto com a semântica de negócios quanto com as construções de uma implementação específica. Esses testes fornecem feedback rápido.

Mas assim que descermos a pirâmide, temos que substituir o meio ambiente por algo. Os stubs aparecem - como serviços completos e entidades individuais da linguagem de programação. É com a ajuda de plugues que podemos testar os componentes isoladamente. Mas eles também reduzem a validade dos cheques. Como garantir que o stub está retornando os dados corretos? Como garantir a sua qualidade?

A solução pode ser uma documentação abrangente que descreve vários cenários e possíveis estados dos componentes do sistema. Mas quaisquer formulações ainda deixam liberdade de interpretação. Portanto, uma boa documentação é um artefato vivo que está em constante aprimoramento à medida que a equipe entende a área do problema. Como garantir a conformidade com os stubs da documentação?

Em muitos projetos, é possível observar uma situação em que os stubs são gravados pelos mesmos caras que desenvolveram o artefato de teste. Por exemplo, os desenvolvedores de aplicativos móveis fazem stubs para seus próprios testes. Como resultado, os programadores podem entender a documentação de sua própria maneira (o que é completamente normal), fazem o stub com o comportamento esperado errado, escrevem o código de acordo com ele (com testes verdes) e são gerados erros reais de integração.

Além disso, a documentação geralmente se move para baixo - os clientes usam especificações de serviços (nesse caso, outro serviço pode ser um cliente do serviço). Ele não expressa como os consumidores usam dados, que dados são necessários, que suposições eles fazem para esses dados. A conseqüência dessa ignorância é a lei de Hyrum .



Hyrum Wright desenvolve ferramentas públicas no Google há muito tempo e observou como as menores alterações podem causar falhas nos clientes que usavam os recursos implícitos (não documentados) de suas bibliotecas. Essa conectividade oculta complica a evolução da API.

Esses problemas podem ser resolvidos em certa medida usando contratos orientados a consumidores. Como qualquer abordagem e ferramenta, ela possui uma gama de aplicabilidade e custo, os quais também consideraremos. As implementações dessa abordagem atingiram um nível de maturidade suficiente para testar seus projetos.

O que é um CDC?


Três elementos-chave:

  • O contrato . Descrito usando algum DSL, dependente da implementação. Ele contém uma descrição da API na forma de cenários de interação: se uma solicitação específica chegar, o cliente deverá receber uma resposta específica.
  • Testes de clientes . Além disso, eles usam um esboço, que é gerado automaticamente a partir do contrato.
  • Testes para a API . Eles também são gerados a partir do contrato.

Assim, o contrato é executável. E a principal característica da abordagem é que os requisitos para o comportamento da API aumentam , do cliente para o servidor.

O contrato se concentra no comportamento que realmente importa para o consumidor. Torna explícitas suas suposições sobre a API.

O principal objetivo do CDC é trazer um entendimento do comportamento da API para seus desenvolvedores e para os desenvolvedores de seus clientes. Essa abordagem está bem combinada com o BDD. Nas reuniões de três amigos, você pode esboçar as lacunas do contrato. Por fim, este contrato também serve para melhorar as comunicações; compartilhando um entendimento comum da área do problema e implementando a solução dentro e entre equipes.

Pacto


Considere usar o CDC como um exemplo do Pact, uma de suas implementações. Suponha que façamos uma aplicação web para os participantes da conferência. Na próxima iteração, a equipe desenvolve um cronograma de apresentação - até o momento, sem histórias como votação ou notas, apenas a saída da grade de relatórios. O código fonte do exemplo está aqui .

Em uma reunião de três quatro amigos, um produto, um testador, os desenvolvedores do back-end e um aplicativo móvel se encontram. Eles dizem que

  • Uma lista com o texto será exibida na interface do usuário: Título do relatório + Alto-falantes + Data e hora.
  • Para fazer isso, o back-end deve retornar dados como no exemplo abaixo.

 { "talks":[ { "title":"      ", "speakers":[ { "name":" " } ], "time":"2019-05-27T12:00:00+03:00" } ] } 

Depois disso, o desenvolvedor do frontend escreve o código do cliente (backend para o frontend). Ele instala uma biblioteca de contratos de pacto no projeto:

 yarn add --dev @pact-foundation/pact 

E começa a escrever um teste. Ele configura o servidor stub local, que simulará o serviço com agendamentos de relatório:

 const provider = new Pact({ //      consumer: "schedule-consumer", provider: "schedule-producer", // ,     port: pactServerPort, //  pact     log: path.resolve(process.cwd(), "logs", "pact.log"), // ,     dir: path.resolve(process.cwd(), "pacts"), logLevel: "WARN", //  DSL  spec: 2 }); 

O contrato é um arquivo JSON que descreve os cenários do cliente interagindo com o serviço. Mas você não precisa descrevê-lo manualmente, pois ele é formado a partir das configurações do stub no código. O desenvolvedor antes do teste descreve o seguinte comportamento.

 provider.setup().then(() => provider .addInteraction({ uponReceiving: "a request for schedule", withRequest: { method: "GET", path: "/schedule" }, willRespondWith: { status: 200, headers: { "Content-Type": "application/json;charset=UTF-8" }, body: { talks: [ { title: "      ", speakers: [ { name: " " } ], time: "2019-05-27T12:00:00+03:00" } ] } } }) .then(() => done()) ); 

Aqui, no exemplo, especificamos a solicitação de serviço esperada específica, mas o pact-js também suporta vários métodos para determinar correspondências .

Por fim, o programador escreve um teste dessa parte do código que usa esse esboço. No exemplo a seguir, iremos chamá-lo diretamente por simplicidade.

 it("fetches schedule", done => { fetch(`http://localhost:${pactServerPort}/schedule`) .then(response => response.json()) .then(json => expect(json).toStrictEqual({ talks: [ { title: "      ", speakers: [ { name: " " } ], time: "2019-05-27T12:00:00+03:00" } ] })) .then(() => done()); }); 

Em um projeto real, isso pode ser um teste rápido de unidade de uma função de interpretação de resposta separada ou um teste lento da interface do usuário para exibir dados recebidos de um serviço.

Durante a execução do teste, o pacto verifica se o stub recebeu a solicitação especificada nos testes. As discrepâncias podem ser vistas como diff no arquivo pact.log.

 E, [2019-05-21T01:01:55.810194 #78394] ERROR -- : Diff with interaction: "a request for schedule" Diff -------------------------------------- Key: - is expected + is actual Matching keys and values are not shown { "headers": { - "Accept": "application/json" + "Accept": "*/*" } } Description of differences -------------------------------------- * Expected "application/json" but got "*/*" at $.headers.Accept 


Se o teste for bem-sucedido, um contrato será gerado no formato JSON. Ele descreve o comportamento esperado da API.

 { "consumer": { "name": "schedule-consumer" }, "provider": { "name": "schedule-producer" }, "interactions": [ { "description": "a request for schedule", "request": { "method": "GET", "path": "/schedule", "headers": { "Accept": "application/json" } }, "response": { "status": 200, "headers": { "Content-Type": "application/json;charset=UTF-8" }, "body": { "talks":[ { "title":"      ", "speakers":[ { "name":" " } ], "time":"2019-05-27T12:00:00+03:00" } ] }}} ], "metadata": { "pactSpecification": { "version": "2.0.0" } } } 

Ele concede esse contrato ao desenvolvedor de back-end. Digamos que a API esteja no Spring Boot. O Pact possui uma biblioteca pact-jvm-provider-spring que pode funcionar com o MockMVC. Mas vamos dar uma olhada no Spring Cloud Contract, que implementa o CDC no ecossistema Spring. Ele usa seu próprio formato de contrato, mas também possui um ponto de extensão para conectar conversores de outros formatos. Seu formato de contrato nativo é suportado apenas pelo próprio Spring Cloud Contract - ao contrário do Pact, que possui bibliotecas para JVM, Ruby, JS, Go, Python, etc.

Suponha que, em nosso exemplo, o desenvolvedor de back-end use Gradle para criar o serviço. Ele conecta as seguintes dependências:

 buildscript { // ... dependencies { classpath "org.springframework.cloud:spring-cloud-contract-pact:2.1.1.RELEASE" } } plugins { id "org.springframework.cloud.contract" version "2.1.1.RELEASE" // ... } // ... dependencies { // ... testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-verifier' } 

E coloca o contrato Pact recebido do frotender no diretório src/test/resources/contracts .

A partir dele, por padrão, o plug-in spring-cloud-contract subtrai contratos. Durante a montagem, a tarefa gradle generateContractTests é executada, o que gera o seguinte teste no diretório build / generate-test-sources.

 public class ContractVerifierTest extends ContractsBaseTest { @Test public void validate_aggregator_client_aggregator_service() throws Exception { // given: MockMvcRequestSpecification request = given() .header("Accept", "application/json"); // when: ResponseOptions response = given().spec(request) .get("/scheduler"); // then: assertThat(response.statusCode()).isEqualTo(200); assertThat(response.header("Content-Type")).isEqualTo("application/json;charset=UTF-8"); // and: DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); assertThatJson(parsedJson).array("['talks']").array("['speakers']").contains("['name']").isEqualTo( /*...*/ ); assertThatJson(parsedJson).array("['talks']").contains("['time']").isEqualTo( /*...*/ ); assertThatJson(parsedJson).array("['talks']").contains("['title']").isEqualTo( /*...*/ ); } } 


Ao iniciar este teste, veremos um erro:

 java.lang.IllegalStateException: You haven't configured a MockMVC instance. You can do this statically 

Como podemos usar ferramentas diferentes para teste, precisamos informar ao plug-in qual deles configuramos. Isso é feito através da classe base, que herdará os testes gerados pelos contratos.

 public abstract class ContractsBaseTest { private ScheduleController scheduleController = new ScheduleController(); @Before public void setup() { RestAssuredMockMvc.standaloneSetup(scheduleController); } } 


Para usar essa classe base durante a geração, você precisa configurar o plug-in gradle spring-cloud-contract.

 contracts { baseClassForTests = 'ru.example.schedule.ContractsBaseTest' } 


Agora, temos o seguinte teste gerado:
 public class ContractVerifierTest extends ContractsBaseTest { @Test public void validate_aggregator_client_aggregator_service() throws Exception { // ... } } 

O teste é iniciado com êxito, mas falha com um erro de verificação - o desenvolvedor ainda não escreveu a implementação do serviço. Mas agora ele pode fazer isso com base em um contrato. Ele pode garantir que ele seja capaz de processar a solicitação do cliente e retornar a resposta esperada.

O desenvolvedor de serviços sabe através do contrato o que ele precisa fazer, que comportamento implementar.

O pacto pode ser integrado mais profundamente no processo de desenvolvimento. Você pode implantar um Pact-broker que agrega esses contratos, oferece suporte à versão e pode exibir um gráfico de dependência.



O upload de um novo contrato gerado para o broker pode ser feito na etapa CI ao criar o cliente. E no código do servidor, indique o carregamento dinâmico do contrato pelo URL. O Spring Cloud Contract também suporta isso.

Aplicabilidade do CDC


Quais são as limitações dos contratos direcionados ao consumidor?

Para usar essa abordagem, você deve pagar com ferramentas adicionais, como o pacto. Os contratos em si são um artefato adicional, outra abstração que deve ser cuidadosamente mantida e aplicada conscientemente às práticas de engenharia.

Eles não substituem os testes e2e , pois os stubs ainda permanecem stubs - modelos de componentes reais do sistema, que podem ser um pouco, mas não correspondem à realidade. Por meio deles, cenários complexos não podem ser verificados.

Além disso, os CDCs não substituem os testes funcionais da API . Eles são mais caros de oferecer suporte do que os Testes de Unidade Antigos Simples. Os desenvolvedores de pacto recomendam o uso das seguintes heurísticas - se você remover o contrato e isso não causar erros ou erros de interpretação pelo cliente, isso não será necessário. Por exemplo, não é necessário descrever absolutamente todos os códigos de erro da API por meio de um contrato se o cliente os processar da mesma maneira. Em outras palavras, o contrato descreve apenas para o serviço o que é importante para seu cliente . Não mais, mas não menos.

Muitos contratos também complicam a evolução da API. Cada contrato adicional é uma ocasião para testes em vermelho . É necessário projetar um CDC de forma que cada teste de falha leve uma carga semântica útil que supere o custo de seu suporte. Por exemplo, se um contrato fixa o comprimento mínimo de um determinado campo de texto que é indiferente ao consumidor (ele usa a técnica Toleran Reader ), todas as alterações nesse valor mínimo quebram o contrato e os nervos das pessoas ao seu redor. Essa verificação precisa ser transferida para o nível da própria API e implementada, dependendo da fonte de restrições.

Conclusão


O CDC melhora a qualidade do produto, descrevendo explicitamente o comportamento de integração. Ajuda clientes e desenvolvedores de serviços a alcançar um entendimento comum, permite que você converse através do código. Mas isso ocorre com o custo de adicionar ferramentas, introduzir novas abstrações e ações adicionais dos membros da equipe.

Ao mesmo tempo, as ferramentas e estruturas do CDC estão sendo ativamente desenvolvidas e já atingiram a maturidade para testar seus projetos. Teste :)

Na conferência QualityConf , de 27 a 28 de maio, Andrei Markelov falará sobre técnicas de teste no produto, e Arthur Khineltsev conversará sobre o monitoramento de um front-end altamente carregado, quando o preço de até um pequeno erro é dezenas de milhares de usuários tristes.

Venha conversar por qualidade!

Source: https://habr.com/ru/post/pt452960/


All Articles