Neste artigo vamos falar sobre Spring Cloud Contract, que é algo novo e que nem todo mundo conhece mas é muito útil para quem está trabalhando com microsserviços. A idéia principal é validar as suas integrações e garantir que o contrato não seja quebrado e que elas funcionem a cada novo deploy.
Vamos iniciar o projeto utilizando o Spring Initializer. Para começar, crie um projeto com as dependências do Cloud Contract Verifier e Web, como na imagem abaixo:
Agora é preciso adicionar o plugin que gera os stubs para que você possa utilizá-los nos seus testes integrados em outros microsserviços. O código que você tem que inserir está abaixo:
<plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>1.1.0.RELEASE</version> <extensions>true</extensions> <configuration> <baseClassForTests>br.com.demo.MockMvcTest</baseClassForTests> </configuration> </plugin>
É possível notar que o plugin possui uma classe base de teste, que é definida dentro da tag configuration do plugin. Essa é a sua classe base onde serão declarados os seus serviços rest que serão testados e terão stubs gerados. Abaixo um exemplo dessa classe:
package br.com.contract.demo; import br.com.contract.demo.api.PaymentRestService; import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc; import org.junit.Before; public class MockMvcTest { @Before public void setup() { RestAssuredMockMvc.standaloneSetup(new PaymentRestService()); } }
Legal, neste exemplo a classe que está sendo testada é a PaymentRestService. Este é um exemplo que eu criei para este artigo, o que esse serviço faz é: executando um GET na url v1/payments/{id}/status caso o {id} seja maior que dez o retorno será um pagamento não aprovado e, caso seja menor que dez, aí então o retorno será um pagamento aprovado. A classe em questão está no exemplo abaixo:
@RestController @RequestMapping("v1/payments/{id}/status") public class PaymentRestService { @GetMapping(produces = APPLICATION_JSON_VALUE) public ResponseEntity<PaymentStatus> getPayment(@PathVariable Integer id) { if (id > 10) { return ResponseEntity.status(HttpStatus.PRECONDITION_FAILED) .body(PaymentStatus.builder().approved(false).build()); } else { return ResponseEntity.ok(PaymentStatus.builder().approved(true).build()); } } }
Agora que nós já entendemos a classe que será testada, devemos criar um script groovy que será responsável pelo teste, o que ele irá fazer é enviar uma requisição e validar se o retorno está ok. O script para testar pagamento aprovado, deve ser igual ao abaixo:
import org.springframework.cloud.contract.spec.Contract Contract.make { request { method 'GET' url '/v1/payments/1/status' headers { contentType(applicationJson()) } } response { status 200 body("""{"approved":true}""") headers { contentType(applicationJson()) } } }
E aqui o script para testar um pagamento não aprovado:
import org.springframework.cloud.contract.spec.Contract Contract.make { request { method 'GET' url '/v1/payments/11/status' headers { contentType(applicationJson()) } } response { status 412 body("""{"approved":false}""") headers { contentType(applicationJson()) } } }
Estes scripts devem obrigatoriamente estar na pasta src/test/resources/contracts
Agora quando você executar o comando mvn clean package você verá que na pasta target foram criados alguns arquivos importantes:
- generated-test-sources/contracts: Nesta pasta está a classe gerada pelo script groovy que, utilizando o RestAssured, valida o seu serviço rest;
- stubs: Nesta pasta estão seus stubs, que podem ser publicados em um nexus para que outros projetos possam utilizá-los no momento dos testes integrados e, com isso, caso aconteça uma atualização o(s) projeto(s) que o utilizam irão ter seus testes quebrados e você não correrá o risco de subir uma alteração com uma integração não funcionando.
Para testar isso localmente execute o mvn clean install, assim os stubs vão para o seu maven local.
Esta foi a primeira parte, agora vamos criar um serviço que irá chamar a api de payment para validar se um pagamento está aprovado. Para isso, vamos criar um projeto no spring initializer com as seguintes dependências: Cloud Contract Stub Runner e Cloud Contract WireMock, igual ao da imagem abaixo:
Após fazer o download do projeto, basta criar uma classe igual a classe abaixo para testar o contrato com o primeiro projeto criado. Esta classe irá fazer uma chamada ao stub, que irá retornar o contrato de acordo com os que foram criados com groovy.
import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; @SpringBootTest @RunWith(SpringRunner.class) @AutoConfigureStubRunner(ids = {"br.com.demo:demo:+:stubs:8080"}, workOffline = true) public class ValidateContractTest { private RestTemplate restTemplate = new RestTemplate(); @Test public void validatePaymentApprovedContract() throws Exception { ResponseEntity<String> responseEntity = restTemplate.getForEntity( "http://127.0.0.1:8080/v1/payments/1/status", String.class); assertThat(responseEntity.getStatusCode(), equalTo(HttpStatus.OK)); assertThat(responseEntity.getBody(), equalTo( "{\"approved\":true}")); } @Test public void validatePaymentNotApprovedContract() { try { restTemplate.getForEntity("http://127.0.0.1:8080/v1/payments/11/status", String.class); } catch (HttpClientErrorException ex) { assertThat(ex.getStatusCode(), equalTo(HttpStatus.PRECONDITION_FAILED)); assertThat(ex.getResponseBodyAsString(), equalTo( "{\"approved\":false}")); } } }
E é isso, agora todas as suas integrações podem ser mocadas e testadas de forma efetiva e acabou aquele problema de “quem removeu o atributo da api?”. Para ver o código acesse o repo.
Abraços, não deixem de comentar as dúvidas e até o próximo artigo!