Testes de Contrato - APIs Orientadas pelos Consumidores
Integrar APIs é uma atividade que demanda análise cuidadosa, uma vez que implica na dependência de um recurso externo por parte de um microserviço. Essa forma de integração acrescenta um nível de risco às operações, uma vez que discrepâncias entre os dados esperados e os recebidos podem resultar em falhas de integração. Assegurar a continuidade da integração frente a alterações nas APIs torna-se, portanto, uma prioridade crucial, visando evitar contratempos que possam impactar diretamente o usuário final.
Uma abordagem de grande valor em cenários que envolvem integração entre APIs é o uso de Testes de Contratos. Esses testes verificam, por meio de requisições, se as chamadas para um serviço externo estão de acordo com o esperado. Eles auxiliam na prevenção de falhas, pois, por meio de asserções, permitem antecipar o feedback de eventuais alterações que possam causar quebras no contrato.
Neste artigo, vamos demonstrar o uso dos testes de contrato de forma prática com alguns microserviços que se integram. O artigo possui três divisões principais, sendo elas:
- Desenvolvimento de três microserviços que se integram;
- Inclusão de testes de contratos para validar as integrações entre os microserviços;
- Simulação de alterações que quebram o contrato para fazer os testes falharem.
O código do projeto está disponível no Github em pact-contract-test.
Sistema de Comercialização de Livros
Para exemplificar o uso dos testes de contrato, vamos utilizar um sistema composto por três microserviços: person-data-service
; book-data-service
; e book-sales-service
.
O microserviço book-sales-service
é responsável por realizar operações de vendas de livros. Para efetivar uma venda, ele depende do person-data-service
para obter dados das pessoas e do book-data-service
para obter dados dos livros e atualizar o estoque.

Serviços do sistema de venda de livros.
Serviço de Dados de Pessoas
O person-data-service
possui uma API que recebe o código da pessoa e retorna os seus dados. Uma requisição para obter os dados de uma pessoa pode ser feita conforme o comando curl
demonstrado abaixo.
curl --location --request \
GET 'http://localhost:8082/persons/1001'
O resultado da requisição retorna os dados da pessoa como demonstrado abaixo.
{
"id" : 35,
"name" : "John",
"country" : "Brazil",
"passportNumber" : FT875654
}
O serviço possui uma estrutura simples composta por: Controllers
; Services
; e Repositories
. O objetivo desse exercício é testar o contrato entre as APIs, sem utilizar integração com banco de dados. Os dados são armazenados em uma coleção em memória. Abaixo está o diagrama com os principais componentes.

Componentes de person-data-service
Serviço de Dados de Livros
O book-data-service
possui duas APIs: uma que recebe o código do livro e retorna seus dados, e outra responsável por atualizar o estoque. O código está estruturado de forma similar ao person-data-service
, ou seja, com Controller
, Service
e Repository
, conforme demonstrado no diagrama abaixo.

Componentes de book-data-service
.
Obter dados dos livros
O comando curl
abaixo demonstra como fazer uma requisição para obter os dados de um livro.
curl --location --request GET 'http://localhost:8081/books/201'
O retorno será no formato JSON
conforme demonstrado abaixo.
{
"id": 202,
"name": "Refactoring: Improving the Design of Existing Code",
"stock": 0,
"isbn": "0201485672"
}
Atualizar o estoque do livro
A atualização do estoque de um livro é feita por meio de uma requisição POST
, como exemplificado abaixo.
curl --location 'http://localhost:8081/books/updateStock' \
--header 'Content-Type: application/json' \
--data '{"id": 202,"quantity": 1}'
A API irá retornar o status da operação com SUCCESS
ou FAILURE
, dependendo do resultado. No caso de sucesso:
{
"status": "SUCCESS",
"message":"The stock of the book 202 was update to 2."
}
Se ocorrer algum problema, uma falha será retornada, como demonstrado no exemplo abaixo, onde a operação resulta em um estoque negativo.
{
"status": "FAILURE",
"message": "Failed to update stock of book 202 because the new stock will be less than 0."
}
Serviço de Venda de Livros
O serviço principal é o book-sales-service
. Este é o serviço responsável por realizar as operações de vendas, orquestrando a integração com outros microserviços.
Para efetuar uma venda, é necessário fornecer o código do livro e o código da pessoa. O serviço então consulta os dados da pessoa no person-data-service
e os dados do livro no book-data-service
. Ao final da operação, faz uma nova chamada ao book-data-service
para atualizar o estoque. A operação pode ser realizada utilizando o comando curl
abaixo.
curl -X POST --location 'http://localhost:8080/book-sales' \
--header 'Content-Type: application/json' \
--data '{"personId": 1001,"bookId": 203}'
Se a venda for realizada com sucesso, uma mensagem será retornada para indicar a conclusão da operação, como exemplificado abaixo.
{
"status": "SUCCESS",
"message": "The sale to person John with passport number FT8966563 was successful."
}
Caso aconteça alguma falha na venda, uma mensagem com mais detalhes é retornada. No exemplo abaixo, um exemplo de venda de um livro que não possui estoque.
{
"status": "OUT_OF_STOCK",
"message": "The current stock of book 201 is 0 and is not sufficient to make the sale."
}
Além do Componentes de Controller
, Service
e Repository
, esse serviço possui os componentes responsáveis por integrar com os provedores, são eles: BookDataServiceWebClient
; e PersonDataServiceWebClient
. O diagrama abaixo demonstra como os componentes estão organizados.
book-sales-service
.
Testes de Contrato
O serviço book-sales-service
depende do book-data-service
e do person-data-service
para realizar uma operação de venda, o que torna essencial garantir a comunicação entre eles.
Os Testes de Contrato são uma maneira eficiente de garantir a integração, evitando problemas decorrentes da quebra de contrato. Pact é uma ferramenta que possui as características necessárias para trabalhar com testes de contrato, oferecendo funcionalidades que permitem que os testes sejam orientados pelos consumidores, ou seja, os consumidores definem qual contrato deve ser seguido.
Pact
Pact permite testar a integração entre provedores e consumidores de dados. Possui uma abordagem simples e fácil de ser colocada em prática, com três pontos principais: Broker
, Provider
e Consumer
.
O broker
é responsável por centralizar os contratos, sendo nele que os Consumers
publicam seus contratos. Os Providers
também utilizam o broker
, porém com o objetivo de verificar se a API está de acordo com o contrato definido pelos Consumers.
O diagrama abaixo, fornecido pela própria documentação do Pact, demonstra em mais detalhes os passos utilizados.

Fonte da imagem: https://docs.pact.io/
Inclusão do Pact para testar os contratos
O microsserviço responsável por consumir outras APIs é encarregado de criar e publicar o contrato. Neste caso, o book-sales-service
é o Consumer
. Os microserviços book-data-service
e person-data-service
são responsáveis por fornecer os dados, sendo eles os Providers
.
Consumers
O primeiro passo é configurar o serviço para utilizar o pact
. Neste exemplo, está sendo utilizado o Gradle
, sendo necessário incluir o plugin
e as dependências no arquivo build.gradle
, conforme demonstrado abaixo.
plugins {
[...]
id 'au.com.dius.pact' version "4.3.15"
}
dependencies {
[...]
testImplementation 'au.com.dius.pact.consumer:junit5:4.3.15'
}
É necessário incluir também a url
do broker
conforme demonstrado abaixo.
pact {
publish {
pactBrokerUrl = "http://localhost:9292"
}
}
Contrato com person-data-service
Para criar o contrato com person-data-service
é necessário uma nova classe que estende PactConsumerTestExt
.
@ExtendWith(PactConsumerTestExt.class)
public class BookSalesConsumerForPersonDataContractTest
Deve-se criar um método com os detalhes do contrato, onde são estabelecidos os requisitos por parte do consumidor. Como pode ser visto abaixo, espera-se que no corpo da resposta sejam retornados o código da pessoa, nome e número do passaporte.
@Pact(consumer = CONSUMER_BOOK_SALES_SERVICE_GET_PERSONS, provider = PROVIDER_PERSON_DATA_SERVICE)
public V4Pact whenRequestPersonById_thenReturnsPerson(PactDslWithProvider builder) {
PactDslJsonBody bodyResponse = new PactDslJsonBody()
.integerType("id", A_PERSON_ID)
.stringType("name", A_PERSON_NAME)
.stringType("passportNumber", A_PERSON_PASSPORT_NUMBER);
return builder
.given("it has a person and status code is 200")
.uponReceiving("a request to retrieve a person by id")
.path("/persons/" +A_PERSON_ID)
.method("GET")
.willRespondWith()
.headers(Collections.singletonMap("Content-Type", "application/json"))
.status(OK.value())
.body(bodyResponse)
.toPact(V4Pact.class);
}
É necessário também um segundo método com objetivo de fazer a interação com o teste. Nele é utilizado MockServer
em conjunto com o WebClient
conforme demonstrado abaixo.
@PactTestFor(providerName = PROVIDER_PERSON_DATA_SERVICE,
pactMethod = "whenRequestPersonById_thenReturnsPerson",
providerType = SYNCH)
@Test
public void whenRequestPersonById_thenReturnsPerson(MockServer mockServer) {
// given
WebClient webClient = WebClient.builder()
.baseUrl(mockServer.getUrl())
.build();
// when
PersonDataServiceWebClient personDataServiceWebClient =
new PersonDataServiceWebClient(webClient, "/persons/{personId}");
PersonDataResponse personDataResponse = personDataServiceWebClient.retrievePerson(A_PERSON_ID);
// then
assertThat(personDataResponse.getId()).isInstanceOf(Long.class).isEqualTo(A_PERSON_ID);
assertThat(personDataResponse.getName()).isInstanceOf(String.class).isEqualTo(A_PERSON_NAME);
assertThat(personDataResponse.getPassportNumber()).isInstanceOf(String.class).isEqualTo(A_PERSON_PASSPORT_NUMBER);
}
Contrato com book-data-service
No caso de book-data-service
são duas dependências, uma para obter os dados do livro e outro para solicitar a atualização do estoque.
@Pact(consumer = CONSUMER_BOOK_SALES_SERVICE_GET_BOOKS, provider = PROVIDER_BOOK_DATA_SERVICE)
public V4Pact whenRequestBookById_thenReturnsBook(PactDslWithProvider builder) {
PactDslJsonBody bodyResponse = new PactDslJsonBody()
.integerType("id", A_BOOK_ID)
.integerType("stock", A_BOOK_STOCK);
return builder
.given("it has a book and status code is 200")
.uponReceiving("a request to retrieve a book by id")
.path("/books/" + A_BOOK_ID)
.method("GET")
.willRespondWith()
.headers(Collections.singletonMap("Content-Type", "application/json"))
.status(OK.value())
.body(bodyResponse)
.toPact(V4Pact.class);
}
@Pact(consumer = CONSUMER_BOOK_SALES_SERVICE_UPDATE_STOCK, provider = PROVIDER_BOOK_DATA_SERVICE)
public V4Pact whenUpdateBookStock_thenReturnsStatus(PactDslWithProvider builder) {
PactDslJsonBody responseBody = new PactDslJsonBody()
.stringType("status", "SUCCESS")
.stringType("message", "The stock of the book " + A_BOOK_ID + " was update to 16");
PactDslJsonBody requestBody = new PactDslJsonBody()
.integerType("id", A_BOOK_ID)
.integerType("quantity", A_BOOK_QUANTITY)
.asBody();
return builder
.given("it has a book and stock can be updated")
.uponReceiving("a request to update the stock of book")
.method(HttpMethod.POST.name())
.path(PATH_UPDATE_STOCK)
.headers(Collections.singletonMap("Content-Type", "application/json"))
.body(requestBody)
.willRespondWith()
.headers(Map.of("Content-type", "application/json"))
.status(OK.value())
.body(responseBody)
.toPact(V4Pact.class);
}
Providers
Com os contratos definidos pelos Consumers
, o próximo passo é preparar a verificação pelos provedores. Os providers são book-data-service
e person-data-service
, sendo necessário criar um teste específico para cada um deles. Nesse caso, a inclusão do Pact
depende da adição da dependência e das configurações no arquivo build.gradle
, conforme demonstrado abaixo.
dependencies {
[...]
testImplementation 'au.com.dius.pact.provider:junit5spring:4.5.6'
}
Algumas variáveis precisam ser definidas para a execução dos testes. São elas: a url
, a versão e se os resultados devem ser publicados com a execução dos testes.
tasks.named('test') {
useJUnitPlatform()
systemProperties["pactbroker.url"] = "http://localhost:9292"
systemProperties["pact.provider.version"] = version
systemProperties["pact.verifier.publishResults"] = "true"
}
Verificação do contrato por person-data-service
Para que não seja necessário iniciar toda a aplicação, é utilizado @WebMvcTest
. Um mock
da classe de serviço é adicionado para que o contexto do teste seja específica para o Controller
.
@WebMvcTest
@Provider("person-data-service")
@PactBroker
public class PersonDataProviderForBookSalesContractTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private PersonService personService;
@BeforeEach
public void setUp(PactVerificationContext context){
context.setTarget(new MockMvcTestTarget(mockMvc));
}
//[...]
}
O mock
deve ser incluído em um método anotado com @State
, que é o ponto do código onde se conecta com o Consumer
. No @State
, é importante que a string definida seja a mesma definida quando se está criando o contrato no consumer
. No caso de pessoa, ela é atribuída no builder de V4Pact
em given("it has a person and status code is 200")
.
@State("it has a person and status code is 200")
public void itHasPersonWithIdAndStatusIs200() {
when(personService.getPersonById(A_PERSON_ID))
.thenReturn(Person.builder()
.id(A_PERSON_ID)
.name(A_PERSON_NAME)
.passportNumber(A_PERSON_PASSPORT_NUMBER)
.build());
}
Por fim, é necessário adicionar o código responsável por realizar a verificação do contrato.
@TestTemplate
@ExtendWith(PactVerificationSpringProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
Verificação do contrato por book-data-service
O mesmo procedimento deve ser feito para o book-data-service
, sendo a única diferença que neste caso há dois contratos: um para retornar os dados do livro e outro para atualizar o estoque. Portanto, são necessários dois @State
, como demonstrado abaixo.
@State("it has a book and status code is 200")
public void itHasBookWithIdAndStatusIs200() {
when(bookService.getBookById(A_BOOK_ID))
.thenReturn(Book.builder()
.id(A_BOOK_ID)
.stock(A_BOOK_STOCK)
.build());
}
@State("it has a book and stock can be updated")
public void aRequestToUpdateTheStockOfBook() {
when(bookService.updateStock(A_BOOK_ID, A_BOOK_QUANTITY_UPDATE_STOCK))
.thenReturn(UpdateStockResult.builder()
.message("The stock of the book " + A_BOOK_ID + " was update to 16")
.status(SUCCESS)
.build());
}
Publicação e Verificação do contrato
Com os testes criados, o próximo passo é publicar o contrato no Pact Broker, permitindo assim que os providers realizem a verificação. É necessário ter uma instância do broker, para isso utilizamos um contêiner. Abaixo está o código responsável pelo contêiner, que faz parte do docker-compose
utilizado para os serviços.
services:
pactbroker:
image: pactfoundation/pact-broker:2.104.0.0
environment:
PACT_BROKER_DATABASE_ADAPTER: sqlite
PACT_BROKER_DATABASE_NAME: pactbroker
ports:
- "9292:9292"
[...]
Para iniciar somente o pact broker
, basta executar o comando abaixo. O broker
estará disponível em http://localhost:9292/
.
docker-compose up pactbroker -d
Uma página semelhante a demonstrada na figura abaixo será exibida.

Página inicial do pact broker.
Publicação do contrato pelo consumer
Com o broker
funcionando, o próximo passo é realizar a publicação do contrato pelo consumidor, o book-sales-service
. Para isso, é necessário estar no diretório do projeto e fazer o build
, conforme demonstrado abaixo.
./gradlew clean build
Isso vai gerar alguns arquivos json
em buid\pacts
. Para cada contrato é gerado um arquivo diferente, como demonstrado abaixo.

Arquivos json
com os contratos.
Basta agora publicar os arquivos para o broker
conforme demonstrado abaixo.
./gradlew pactPublish
Ao acessar o Contratos publicados pelo consumidor.broker
, será possível visualizar os três contratos publicados.
Verificação do contrato pelos providers
Com os contratos publicados, o próximo passo é fazer a verificação pelos provedores. Nesta etapa, é possível impedir a liberação de qualquer alteração que quebre o contrato.
São dois serviços que precisam verificar os contratos. Vamos começar com o person-data-service
. Dentro do projeto, basta realizar o clean
e o build
, pois assim os testes serão executados.
./gradlew clean build
Se tudo ocorrer conforme esperado, é então exibido na coluna Last verified
a verificação realizada, conforme demonstrado na imagem abaixo.

Verificação do contrato por person-data-service
.
O mesmo procedimento deve ser realizado para book-data-service
, dentro do projeto fazer o clean
e build
do projeto.
./gradlew clean build
Se os testes passarem, o broker
é então atualizado com as verificações realizadas.

Verificação do contrato por book-data-service
.
Simulando a quebra de contratos
Um grande benefício do uso de testes de contrato é a capacidade de garantir que os provedores possam fazer alterações sem afetar negativamente os consumidores. Para testar esse cenário, vamos simular algumas alterações e observar como o Pact
responde a esse tipo de situação.
Simulação 1: Provider remove campo da API
No primeiro cenário, o person-data-service
realiza uma alteração removendo o campo número do passaporte, essencial para o serviço book-sales-service
. Com a remoção do campo, a resposta do serviço fica da seguinte forma.
{
"id":1002,
"name":"Maria",
"country":"Brazil"
}
Essa simulação de alteração está disponível em um branch
, para acessar basta fazer o checkout
.
git checkout simulation-1-person-data-remove-passport-number
Com a alteração realizada, o próximo passo é executar os testes de contrato do person-data-service
. Para isso, o comando abaixo pode ser utilizado.
./gradlew clean build
O teste falhará e a razão pode ser visualizada no relatório gerado.
Relatório com detalhes sobre falha.
O Matriz de verificação do contrato.broker
também sinalizará uma quebra no contrato na matriz de verificação.
Mais detalhes está disponível ao clicar no item da matriz com a falha.
Detalhes sobre a falha na verificação.
Simulação 2: Provider renomeia campo da API
Na segunda simulação, o serviço book-data-service
altera o nome do campo stock
para currentStock
. Com essa modificação, abaixo está o novo corpo de resposta da API.
{
"id":201,
"name":"Domain-Driven Design: Tackling Complexity in the Heart of Software",
"currentStock":0,
"isbn":"9780321125217"
}
Essa simulação de alteração também está disponível em um branch.
git checkout simulation-2-book-data-change-stock-field-name
Para ver o teste quebrar, basta fazer o build do projeto.
./gradlew clean build
Um relatório com a falha também será gerado, apontando mais detalhes sobre a quebra de contrato.
Relatório com detalhes sobre falha.
A matriz de verificação aponta mais detalhes sobre o histórico de verificação.
Matriz de verificação do contrato.
Para ver mais detalhes sobre a falha, basta clicar no item da matriz.
Detalhes sobre a falha na verificação.
Simulação 3: Provider renomeia campo em API Post
Na terceira simulação, ocorre a mudança do nome do campo de quantity
para quantityToUpdate
na requisição de atualização do estoque para o microserviço book-data-service
. Abaixo está o novo corpo da requisição com essa alteração.
{
"id": 202,
"quantityToUpdate": 1
}
Essa simulação de alteração está disponível em um branch
.
git checkout simulation-3-book-data-rename-field-name-post-request
Para ver o teste quebrar, basta fazer o build do projeto.
./gradlew clean build
O relatório com a falha será gerado, apontando mais detalhes sobre a quebra de contrato. Nesse caso, é esperado sucesso na operação, porém é retornado código 400 sinalizando que o campo quantityToUpdate
não foi enviado no corpo da requisição.

Relatório com detalhes sobre falha.
A matriz de verificação aponta mais detalhes sobre o histórico de verificação.
Matriz de verificação do contrato.
Para ver mais detalhes sobre a falha, basta clicar no item da matriz.
Detalhes sobre a falha na verificação.
Conclusão
A utilização de testes de contrato em ambientes com múltiplos microserviços simplifica a prevenção de quebras de contrato que possam afetar a integração entre as APIs. Embora demande um esforço adicional para preparar o ambiente e criar os contratos, uma vez feito isso, os serviços se tornam mais seguros e menos vulneráveis a falhas. Abaixo estão alguns dos benefícios ao utilizar testes de contrato:
- Os contratos tornam os serviços menos vulneráveis a falhas na integração, já que alterações que possam impactá-los passam a quebrar o contrato.
- Provedores que têm vários consumidores deixam explícito cada contrato. Assim, é mais fácil saber quais serviços dependem de algo específico e planejar alterações de forma mais segura.
- Refatorações tornam-se mais seguras, pois sabe-se exatamente o que cada consumidor utiliza na API. Um exemplo são campos incluídos sem justificativa e ninguém sabe ao certo por que estão ali.
- É uma maneira de padronizar os contratos entre microserviços em um cenário com várias equipes.
- Documentações são muito importantes, mas mais suscetíveis a falhas. Testes de contrato são uma forma automatizada que pode ser configurada para quebrar o
build
da aplicação em caso de falha de contrato. - É uma abordagem que se assemelha ao
TDD (Test-Driven Development)
, onde os testes criados no provider são guiados pelos testes criados nos consumidores.
Referências
- Diagramas criados utilando o C4 Model. Mais detalhes em https://c4model.com/.
- Pact Documentation. https://docs.pact.io/.
- Contract Test. Martin Fowler. Disponível em https://martinfowler.com/bliki/ContractTest.html.
- Consumer-Driven Contracts: A Service Evolution Pattern. Ian Robinson. https://martinfowler.com/articles/consumerDrivenContracts.html.
- Testing the Web Layer. https://spring.io/guides/gs/testing-web.
- Pact Spring/JUnit5 Support. https://docs.pact.io/implementation_guides/jvm/provider/junit5spring.