📜 ⬆️ ⬇️

Web application integration using Spring Cloud Contract


The article focuses on the integration of web applications written with Spring and working over HTTP. The name Spring Cloud Contract, in my opinion, is misleading, since it has nothing to do with the cloud.


It's about API contracts.


For unit testing of controllers, mockMCV or RestAssured are used quite often. For mocks on the frontend side, moscow servers are used, such as Wiremock or Pact. But often, unit tests are written by some people, and moki by others.


This can lead to prolifem integration.


For example, a server with no data may return 204 NO_CONTENT, while a client may expect 200 OK and an empty json.


It does not matter which of them is right. The problem is that someone made a mistake and it will be found not earlier than the integration stage.


This problem is intended to solve spring cloud contract.


What is spring cloud contract


This is a file in which the yaml or groovyDSL dialect describes how the request and response should look. By default, all contracts are in the /src/test/resources/contracts/* folder.


For example, let's test the simplest GET-endpoint


 @GetMapping("/bets/{userId}") public ResponseEntity<List<Bet>> getBets(@PathVariable("userId") String userId) { List<Bet> bets = service.getByUserId(userId); if (bets.isEmpty()) { return ResponseEntity.noContent().build(); } return ResponseEntity.ok(bets); } 

We describe the contract


 org.springframework.cloud.contract.spec.Contract.make { request { method 'GET' urlPath '/bets/2' } response { status 200 headers { header('Content-Type', 'application/json') } body(''' { "sport":"football", "amount": 1 } ''' ) } } 

Further from this file using maven or gradle plugin generates unit tests and json's for wiremock .


JSON description of mock for the example above will look like this:


 { "id" : "df8f7b73-c242-4664-add3-7214ac6356ff", "request" : { "urlPath" : "/bets/2", "method" : "GET" }, "response" : { "status" : 200, "body" : "{\"amount\":1,\"sport\":\"football\"}", "headers" : { "Content-Type" : "application/json" }, "transformers" : [ "response-template" ] }, "uuid" : "df8f7b73-c242-4664-add3-7214ac6356ff" } 

Wiremock can be run locally, you just need to download the jar from here . By default, json-moki should put the mappings folder.


 $java -jar wiremock-standalone-2.18.0.jar 

Below is the generated test. The default library is RestAssured, but mockMVC or spockframework can be used.


 public class UserControllerTest extends ContractBae { @Test public void validate_get_200() throws Exception { // given: MockMvcRequestSpecification request = given(); // when: ResponseOptions response = given().spec(request) .get("/bets/2"); // then: assertThat(response.statusCode()).isEqualTo(200); assertThat(response.header("Content-Type")).isEqualTo("application/json"); // and: DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); assertThatJson(parsedJson).field("['amount']").isEqualTo(1); assertThatJson(parsedJson).field("['sport']").isEqualTo("football"); } } 

It should be noted that all generated classes inherit some base class (there may be several base classes) in which all the necessary parameters for tests are initialized. The path to the base class is described in the plugin settings.


For this example, the base class might look like this:


 @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SccDemoApplication.class) public abstract class ContractBae { @LocalServerPort int port; @Autowired protected WebApplicationContext webApplicationContext; @Before public void configureRestAssured() { RestAssured.port = port; MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .build(); RestAssuredMockMvc.mockMvc(mockMvc); } } 

As a result, we obtain that the tests and moki are obtained from the same source. If unit tests pass and the conditional frontend works on mocks, then there will be no problems with the integration.


But that is not all
Moki can use not only the frontend, but the application itself for integration with another application. Spring is able to run the Moscow server, you only need to generate a jar with mocks and pass the path to it annotation @AutoConfigureStubRunner


Suppose that our controller does HTTP to another application:


 @GetMapping("/bets/{userId}") public ResponseEntity<List<Bet>> getBets(@PathVariable("userId") String userId) { if(!isUsetExists(userId)) { return ResponseEntity.notFound().build(); } List<Bet> bets = service.getByUserId(userId); if (bets.isEmpty()) { return ResponseEntity.noContent().build(); } return ResponseEntity.ok(bets); } private boolean isUsetExists(String userId) { try { restTemplate.getForObject("/exists/" + userId, Void.class); return true; } catch (HttpStatusCodeException ignore) { return false; } } 

Then you just need to describe let the jar with moki in the base class


 @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SccDemoApplication.class) @AutoConfigureStubRunner(ids = {"me.dehasi.contracts.demo:sub-service-stubs:+:stubs:8090"}, stubsMode = StubRunnerProperties.StubsMode.LOCAL) public abstract class ContractBase { 

Since these are tests of the controller, then from the same tests you can generate ascii-doc snippets (a full-featured article about rest-docs already exists in Habré ).


What we have
It turns out that we have one source of the contract API, which is described in human-readable language, and from it we generate unit tests (theoretically also documentation), and from it the same moki. This approach reduces the risk of integration errors between web applications.


Examples can be viewed on the official website for example .


Code examples in the article are taken from the simplest project here .


')

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


All Articles