Contratos orientados ao consumidor com Pacto
1. Visão geral
Neste artigo rápido, examinaremos o conceito de contratos orientados ao consumidor.
Estaremos testando a integração com um serviço REST externo por meio de um contrato que definimos usando a bibliotecaPact. That contract can be defined by the client, then picked up by the provider and used for development of its services.
Também criaremos testes com base no contrato para os aplicativos cliente e provedor.
2. O que éPact?
Using Pact, we can define consumer expectations for a given provider (that can be an HTTP REST service) in the form of a contract (daí o nome da biblioteca).
Vamos definir este contrato usando o DSL fornecido porPact. Uma vez definidos, podemos testar as interações entre os consumidores e o provedor usando o serviço simulado criado com base no contrato definido. Além disso, testaremos o serviço em relação ao contrato usando um cliente simulado.
3. Dependência do Maven
Para começar, precisaremos adicionar a dependência Maven à bibliotecapact-jvm-consumer-junit_2.11:
au.com.dius
pact-jvm-consumer-junit_2.11
3.5.0
test
4. Definindo um Contrato
Quando queremos criar um teste usandoPact, primeiro precisamos definir um@Rule que será usado em nosso teste:
@Rule
public PactProviderRuleMk2 mockProvider
= new PactProviderRuleMk2("test_provider", "localhost", 8080, this);
Estamos passando o nome do provedor, o host e a porta em que a simulação de servidor (que é criada a partir do contrato) será iniciada.
Digamos que o serviço tenha definido o contrato para dois métodos HTTP que ele pode manipular.
O primeiro método é uma solicitação GET que retorna JSON com dois campos. Quando a solicitação é bem-sucedida, ele retorna um código de resposta HTTP 200 e o cabeçalho Content-Type para JSON.
Vamos definir esse contrato usandoPact.
We need to use the @Pact annotation and pass the consumer name for which the contract is defined. Dentro do método anotado, podemos definir nosso contrato GET:
@Pact(consumer = "test_consumer")
public RequestResponsePact createPact(PactDslWithProvider builder) {
Map headers = new HashMap<>();
headers.put("Content-Type", "application/json");
return builder
.given("test GET")
.uponReceiving("GET REQUEST")
.path("/pact")
.method("GET")
.willRespondWith()
.status(200)
.headers(headers)
.body("{\"condition\": true, \"name\": \"tom\"}")
(...)
}
Usando a DSLPact, definimos que, para uma determinada solicitação GET, queremos retornar uma resposta 200 com cabeçalhos e corpo específicos.
A segunda parte do nosso contrato é o método POST. Quando o cliente envia uma solicitação POST para o caminho/pact com um corpo JSON adequado, ele retorna um código de resposta 201 HTTP.
Vamos definir esse contrato comPact:
(...)
.given("test POST")
.uponReceiving("POST REQUEST")
.method("POST")
.headers(headers)
.body("{\"name\": \"Michael\"}")
.path("/pact")
.willRespondWith()
.status(201)
.toPact();
Observe que precisamos chamar o métodotoPact() no final do contrato para retornar uma instância deRequestResponsePact.
4.1. Artefato de Pacto Resultante
Por padrão, os arquivos do Pact serão gerados na pastatarget/pacts. Para personalizar este caminho, podemos configurar omaven-surefire-plugin:
org.apache.maven.plugins
maven-surefire-plugin
target/mypacts
...
A compilação do Maven irá gerar um arquivo chamadotest_consumer-test_provider.json na pastatarget/mypacts que contém a estrutura das solicitações e respostas:
{
"provider": {
"name": "test_provider"
},
"consumer": {
"name": "test_consumer"
},
"interactions": [
{
"description": "GET REQUEST",
"request": {
"method": "GET",
"path": "/"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"condition": true,
"name": "tom"
}
},
"providerStates": [
{
"name": "test GET"
}
]
},
{
"description": "POST REQUEST",
...
}
],
"metadata": {
"pact-specification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.0"
}
}
}
5. Testando o cliente e fornecedor usando o contrato
Agora que temos nosso contrato, podemos usá-lo para criar testes contra ele, tanto para o cliente quanto para o provedor.
Cada um desses testes usará uma simulação de sua contraparte, com base no contrato, o que significa:
-
o cliente usará um provedor simulado
-
o provedor usará um cliente simulado
Efetivamente, os testes são feitos em relação ao contrato.
5.1. Testando o cliente
Depois de definir o contrato, podemos testar as interações com o serviço que será criado com base nesse contrato. We can create normal JUnit test but we need to remember to put the @PactVerification annotation at the beginning of the test.
Vamos escrever um teste para a solicitação GET:
@Test
@PactVerification()
public void givenGet_whenSendRequest_shouldReturn200WithProperHeaderAndBody() {
// when
ResponseEntity response = new RestTemplate()
.getForEntity(mockProvider.getUrl() + "/pact", String.class);
// then
assertThat(response.getStatusCode().value()).isEqualTo(200);
assertThat(response.getHeaders().get("Content-Type").contains("application/json")).isTrue();
assertThat(response.getBody()).contains("condition", "true", "name", "tom");
}
The @PactVerification annotation takes care of starting the HTTP service. No teste, precisamos apenas enviar a solicitação GET e afirmar que nossa resposta está de acordo com o contrato.
Vamos adicionar o teste para a chamada do método POST também:
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
String jsonBody = "{\"name\": \"Michael\"}";
// when
ResponseEntity postResponse = new RestTemplate()
.exchange(
mockProvider.getUrl() + "/create",
HttpMethod.POST,
new HttpEntity<>(jsonBody, httpHeaders),
String.class
);
//then
assertThat(postResponse.getStatusCode().value()).isEqualTo(201);
Como podemos ver, o código de resposta para a solicitação POST é igual a 201 - exatamente como foi definido no contratoPact.
Como estávamos usando a anotação@PactVerification(), a bibliotecaPact está iniciando o servidor web com base no contrato definido anteriormente antes de nosso caso de teste.
5.2. Testando o Provedor
A segunda etapa da verificação do contrato é criar um teste para o provedor usando um cliente simulado, com base no contrato.
A implementação do nosso provedor será conduzida por este contrato no estilo TDD.
Para nosso exemplo, usaremos uma API Spring Boot REST.
Primeiro, para criar nosso teste JUnit, precisaremos adicionar a dependênciapact-jvm-provider-junit_2.11:
au.com.dius
pact-jvm-provider-junit_2.11
3.5.0
test
Isso nos permite criar um teste JUnit usandoPactRunnere especificando o nome do provedor e a localização do artefato do Pacto:
@RunWith(PactRunner.class)
@Provider("test_provider")
@PactFolder("pacts")
public class PactProviderTest {
//...
}
Para que essa configuração funcione, temos que colocar o arquivotest_consumer-test_provider.json na pastapacts do nosso projeto de serviço REST.
Em seguida, definiremos o destino a ser usado para verificar as interações no contrato e iniciaremos o aplicativo Spring Boot antes de executar os testes:
@TestTarget
public final Target target = new HttpTarget("http", "localhost", 8082, "/spring-rest");
private static ConfigurableWebApplicationContext application;
@BeforeClass
public static void start() {
application = (ConfigurableWebApplicationContext)
SpringApplication.run(MainApplication.class);
}
Por fim, especificaremos os estados do contrato que queremos testar:
@State("test GET")
public void toGetState() { }
@State("test POST")
public void toPostState() { }
A execução dessa classe JUnit executará dois testes para as duas solicitações GET e POST. Vamos dar uma olhada no log:
Verifying a pact between test_consumer and test_provider
Given test GET
GET REQUEST
returns a response which
has status code 200 (OK)
includes headers
"Content-Type" with value "application/json" (OK)
has a matching body (OK)
Verifying a pact between test_consumer and test_provider
Given test POST
POST REQUEST
returns a response which
has status code 201 (OK)
has a matching body (OK)
Observe que não incluímos o código para criar um serviço REST aqui. O serviço completo e teste podem ser encontrados emGitHub project.
6. Conclusão
Neste tutorial rápido, vimos os contratos direcionados ao consumidor.
Criamos um contrato usando a bibliotecaPact. Uma vez definido o contrato, fomos capazes de testar o cliente e o serviço em relação ao contrato e afirmar que eles estão em conformidade com a especificação.
A implementação de todos esses exemplos e trechos de código pode ser encontrada emGitHub project - este é um projeto Maven, portanto, deve ser fácil de importar e executar como está.