Testes de Contrato (CDC), Parte 1: Produtor
Ao finalizarmos esta aula, nós:
- Estaremos familiarizados com os conceitos básicos da abordagem Consumer-Driven Contracts (Contratos Guiados pelo Consumidor).
- Saberemos identificar os principais recursos do Spring Cloud Contract.
- Saberemos criar e configurar um projeto Gradle (Kotlin DSL) para um serviço produtor REST.
- Saberemos criar o nosso primeiro contrato guiado pelo consumidor (consumer-driven contract).
- Entenderemos como podemos criar um serviço produtor REST que cumpra o nosso contrato.
Vamos começar dando uma rápida olhada nos contratos guiados pelo consumidor (consumer-driven contracts).
Obtenha o código-fonte de referência no GitHub
Uma Breve Introdução aos Contratos Guiados pelo Consumidor (Consumer-Driven Contracts)
A abordagem de contratos guiados pelo consumidor (consumer-driven contracts — CDC) nos ajuda a escrever aplicações que utilizam a arquitetura de microsserviços. Quando estamos escrevendo uma aplicação utilizando a abordagem de contratos guiados pelo consumidor, precisamos implementar os seguintes componentes:
- Um provedor (provider / produtor) é um componente que fornece uma API pública que pode ser consumida por outros componentes.
- Um consumidor (consumer) é um componente que consome a API fornecida por um provedor.
- Um contrato (contract) especifica uma interação entre um consumidor e um provedor. Para ser mais específico, ele define a resposta que deve ser retornada pelo provedor quando este recebe a requisição especificada pelo contrato.
Nota: A abordagem de contratos guiados pelo consumidor é um tópico bastante amplo e complexo, sendo impossível cobrir todos os seus detalhes teóricos nesta aula. Se você quiser obter mais informações sobre a abordagem Consumer-Driven Contracts, recomendo a leitura do seguinte artigo clássico: Consumer-Driven Contracts: A Service Evolution Pattern.
A seguir, daremos uma rápida olhada nos principais recursos do Spring Cloud Contract.
Os Principais Recursos do Spring Cloud Contract
O projeto Spring Cloud Contract integra diferentes tecnologias entre si, e seu objetivo é nos ajudar a desenvolver aplicações utilizando contratos guiados pelo consumidor. Seus principais recursos são:
- Podemos criar contratos utilizando Kotlin DSL, Groovy DSL ou YAML.
- O Spring Cloud Contract gera classes de teste que nos ajudam a garantir que os nossos produtores honrem os contratos criados.
- O Spring Cloud Contract gera stubs que fornecem dados de teste permitindo escrever testes para os nossos consumidores. Em outras palavras, esses stubs nos ajudam a garantir que os nossos consumidores consigam consumir os dados retornados pelos nossos produtores.
Nota: Se você quiser obter mais informações sobre os recursos do Spring Cloud Contract, consulte a página oficial do projeto Spring Cloud Contract.
Vamos avançar e descobrir como podemos configurar um projeto Gradle para um serviço produtor REST.
Criando um Projeto Gradle para um Serviço Produtor REST
Como a nossa aplicação de exemplo utiliza o Spring Boot 4.1.2 com Kotlin 2.4.21, o nosso projeto no Gradle 9.8.2 (Kotlin DSL) é basicamente um projeto Spring Boot padrão. No entanto, há três pontos importantes que quero destacar (que também possuem equivalentes diretos caso você utilize Maven):
Primeiro, precisamos adicionar o BOM (Bill of Materials) do Spring Cloud Contract ao nosso build. Esse BOM especifica as versões compatíveis das dependências necessárias do Spring Cloud Contract 5.0. Podemos fazer isso adicionando o seguinte trecho ao nosso arquivo build.gradle.kts:
|
|
Equivalente no Maven (
pom.xml):
1 2 3 4 5 6 7 8 9 10 11<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-dependencies</artifactId> <version>5.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
Segundo, como queremos criar contratos e gerar classes de teste automaticamente, precisamos declarar a dependência spring-cloud-starter-contract-verifier no nosso arquivo de build. Podemos fazer isso adicionando o seguinte trecho à seção dependencies do nosso build.gradle.kts:
|
|
Equivalente no Maven (
pom.xml):
1 2 3 4 5<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-contract-verifier</artifactId> <scope>test</scope> </dependency>
Terceiro, precisamos configurar o plugin do Spring Cloud Contract (Gradle Plugin ou Maven Plugin). Esse plugin gera testes a partir dos contratos fornecidos e cria stubs que podem ser usados para testar o consumidor que consome a nossa API REST. Podemos configurar este plugin seguindo estes passos:
- Adicionar o plugin do Spring Cloud Contract (
org.springframework.cloud.contract) ao nosso build. - Garantir que o plugin adicione as bibliotecas necessárias ao class loader e utilize o framework de testes adequado (JUnit Jupiter 6.0.3). No Maven, habilitar
<extensions>true</extensions>é um passo crucial — se não fizermos isso, o plugin não conseguirá gerar as classes de teste nem criar os stubs necessários. - Configurar o nome totalmente qualificado (fully qualified class name) da classe base que será estendida pelas classes de teste geradas.
Depois de configurarmos o plugin do Spring Cloud Contract no nosso build.gradle.kts, a sua configuração ficará da seguinte forma:
|
|
Equivalente no Maven (
pom.xml):
1 2 3 4 5 6 7 8 9 10<plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>5.0.0</version> <extensions>true</extensions> <configuration> <testFramework>JUNIT5</testFramework> <baseClassForTests>com.testwithspring.master.webinar.BaseTest</baseClassForTests> </configuration> </plugin>
Leitura Adicional:
Agora criamos um projeto configurado que pode compilar e executar o nosso serviço produtor REST. A seguir, implementaremos o nosso serviço produtor REST.
Implementando um Serviço Produtor REST
Quando queremos implementar uma API REST que retorna as informações de uma única tarefa (task), precisamos seguir estes passos:
Primeiro, precisamos criar uma classe que contenha as informações da tarefa retornada. Depois de escrevermos a classe Task em Kotlin, o seu código-fonte ficará da seguinte forma:
|
|
Segundo, precisamos implementar um serviço que retorne as informações da tarefa solicitada. Como este é apenas um exemplo didático, podemos escrever esse serviço seguindo estes passos:
- Adicionar uma propriedade
Mapà nossa classe de serviço. Esse mapa contém as tarefas retornadas pelo nosso serviço. - Inicializar essa estrutura com duas tarefas no nosso mapa (
1Le2L). - Escrever um método
findById()que obtém a tarefa solicitada a partir do nosso mapa.
Depois de escrevermos a nossa classe de serviço, o seu código-fonte ficará da seguinte forma:
|
|
Terceiro, antes de escrevermos o controller que implementa a nossa API REST, temos que criar o contrato que especifica o seu comportamento. Ao criarmos esse contrato, devemos seguir estas duas regras:
- Devemos colocar os nossos contratos no diretório
src/test/resources/contracts(ousrc/contractTest/resources/contractsna convenção padrão do Gradle moderno). - Podemos especificar os nossos contratos utilizando Groovy DSL, Kotlin DSL ou YAML.
Podemos agora criar o nosso contrato seguindo estes passos:
- Criar um novo contrato invocando o método
makeda classeContract. - Fornecer uma descrição (
description) para o nosso contrato. - Identificar a requisição HTTP (
request) que aciona o nosso contrato. No nosso caso, o contrato é válido quando a nossa API REST recebe uma requisiçãoGETpara o caminho/api/task/1. - Especificar a resposta HTTP (
response) que deve ser retornada pela nossa API REST. Neste caso, a nossa API REST deve retornar o status HTTP200, cabeçalho JSON e as informações da tarefa encontrada em formato JSON.
Depois de escrevermos o nosso contrato (find_task_by_id.groovy) utilizando a DSL Groovy, ele ficará da seguinte forma:
|
|
Leitura Adicional:
Quando compilamos o nosso projeto, o plugin do Spring Cloud Contract cria automaticamente uma classe de teste que testa a nossa API REST utilizando o REST Assured (RestAssuredMockMvc) com JUnit Jupiter 6.0.3. Essa classe de teste estende a classe BaseTest, e o seu código-fonte gerado se parece com o seguinte:
|
|
Quarto, precisamos escrever a classe controller que processa requisições GET enviadas para o caminho /api/task/{id}. Esse controller obtém a tarefa retornada invocando o método findById() da classe TaskService.
Depois de escrevermos a nossa classe controller, o seu código-fonte ficará da seguinte forma:
|
|
Quinto, precisamos escrever uma classe abstract class BaseTest que configura o sistema sob teste (system under test) utilizando um método de configuração (setup) executado antes de cada método de teste (@BeforeEach no JUnit Jupiter 6). Podemos implementar esse método de configuração seguindo estes passos:
- Criar um novo mock de
TaskService. - Configurar um novo objeto
MockMvcutilizando a configuração standalone doRestAssuredMockMvc. Precisamos garantir que o objeto criado dê suporte ao REST Assured e registrar o controller testado (TaskController). - Garantir que o nosso objeto mock retorne as informações da tarefa encontrada (
Task(1L, "Write our first contract")) quando for invocado utilizando o parâmetro de método1L.
Depois de escrevermos o nosso método de configuração, o código-fonte da classe BaseTest ficará da seguinte forma:
|
|
Nota: A propósito, também podemos escrever uma classe base que carregue o contexto de aplicação completo da nossa aplicação Spring Boot (por exemplo, com
@SpringBootTest). Isso nos permite testar a nossa API REST utilizando testes de integração completos. Por exemplo, podemos ler informações diretamente de um banco de dados real gerenciado via Testcontainers ou ferramentas de dataset.
Quando executamos os nossos testes (./gradlew check ou ./gradlew contractTest), vemos que todos eles passam! Acabamos de escrever um serviço produtor que cumpre integralmente o nosso contrato. Vamos resumir o que aprendemos nesta aula.
Resumo
Esta aula nos ensinou cinco coisas fundamentais:
- Os contratos especificam as interações entre um produtor (producer / provider) e um consumidor (consumer).
- Podemos criar contratos utilizando Groovy DSL, Kotlin DSL ou YAML.
- O plugin do Spring Cloud Contract (Gradle ou Maven) gera tanto as classes de teste quanto os stubs do produtor.
- As classes de teste geradas pelo plugin do Spring Cloud Contract estendem uma classe base (
BaseTest) que deve ser implementada por nós. - A classe base pode utilizar a configuração standalone do MockMvc ou carregar o contexto de aplicação do Spring. Em outras palavras, podemos escrever tanto testes unitários quanto testes de integração para o nosso produtor.