Introduction to Spring Cloud Contract, Part Two
After we have finished this webinar, we:
- Can create a Maven project for a REST consumer service.
- Know how we can implement a consumer service that consumes the API provided by the provider service.
- Understand how we can ensure that the contract between our consumer and producer hasn’t changed.
- Can write tests for our consumer service.
This webinar assumes that:
Let’s start by creating a Maven project for a REST consumer service.
Creating a Maven Project for a REST Consumer Service
Because our example application uses Spring Boot, our Maven project is pretty much a normal Spring Boot project. However, there are two things I want point out:
First, we have to add the Spring Cloud Contract BOM to our Maven build. This BOM specifies the dependency versions of the Spring Cloud Contract dependencies. If we want to add this BOM to our Maven build, we have to add the following snippet to our pom.xml file:
1<dependencyManagement>
2 <dependencies>
3 <dependency>
4 <groupId>org.springframework.cloud</groupId>
5 <artifactId>spring-cloud-contract-dependencies</artifactId>
6 <version>2.0.1.RELEASE</version>
7 <type>pom</type>
8 <scope>import</scope>
9 </dependency>
10 </dependencies>
11</dependencyManagement>
Second, because we want to replace the actual producer service with a stub, we have to declare the spring-cloud-starter-contract-stub-runner dependency in our POM file. This component can download and run the WireMock stubs generated by our REST producer Maven project. We can declare this dependency by adding the following snippet to the dependencies section of our POM file:
1<dependency>
2 <groupId>org.springframework.cloud</groupId>
3 <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
4 <scope>test</scope>
5</dependency>
We have now created a Maven project that can compile and run our REST consumer service. Next, we will implement our REST consumer service.
Implementing a REST Consumer Service
As we remember , our REST producer service returns the information of one task. Next, we will write a REST consumer service which invokes our REST producer service and returns the information that’s returned by our REST producer service. We can write our consumer service by following these steps:
First, we have to create a class that contains the information of a task. After we have written the Task class, its source code looks as follows:
1public class Task {
2
3 private final Long id;
4 private final String title;
5
6 public Task(Long id, String title) {
7 this.id = id;
8 this.title = title;
9 }
10
11 //Getters are omitted
12}
Second, we have to write the actual REST consumer service by following these steps:
- Create a new controller class and ensure that its controller methods bound the returned value to body of the returned HTTP response.
- Add a
RestTemplatefield to our controller class and write a constructor that injects the actualRestTemplateobject into that field. - Write a controller method that processes
GETrequests send to the path: ‘/api/task/{id}’. - Get the requested task from our REST producer service and return the found task.
After we have written our controller class, its source code looks as follows:
1import org.springframework.boot.web.client.RestTemplateBuilder;
2import org.springframework.web.bind.annotation.GetMapping;
3import org.springframework.web.bind.annotation.PathVariable;
4import org.springframework.web.bind.annotation.RestController;
5import org.springframework.web.client.RestTemplate;
6
7@RestController
8class TaskController {
9
10 private final RestTemplate restTemplate;
11
12 TaskController(RestTemplateBuilder restTemplateBuilder) {
13 this.restTemplate = restTemplateBuilder.build();
14 }
15
16 @GetMapping("/api/task/{id}")
17 Task findById(@PathVariable("id") Long id) {
18 return restTemplate.getForObject(
19 "http://localhost:8080/api/task/{id}",
20 Task.class,
21 id
22 );
23 }
24}
Before we continue, I want to point our that our producer and consumer services listen to different ports. These ports are:
- Our REST producer service listens to the port 8080.
- Our REST consumer service listens to the port 9000.
We have now written our REST consumer service. Let’s move on and find our how we can write tests for our consumer service.
Writing Tests for Our REST Consumer Service
When we write tests for a REST consumer service, we can ensure that the contract provided by the REST producer service hasn’t changed or we can verify that the REST consumer service is working as expected.
Next, we will find out how we can ensure that the contract provided by our REST producer service hasn’t changed.
Writing Tests for Our Contract
If we want to ensure that the contract provided by our REST producer service hasn’t changed, we have to follow these steps:
First, we have to create a new test class and configure the system under test by following these steps:
- Ensure that our tests are run by the
SpringRunnerclass. - Enable the testing enhancements provided by Spring Boot.
- Ensure that our test methods use the WireMock stubs generated by our REST producer Maven project. When we do this, we have to ensure that these stubs are fetched from the local Maven repository. Also, we have to ensure that these stubs listen to the port 8080.
After we have configured the system under test, the source code of our test class looks as follows:
1import org.junit.runner.RunWith;
2import org.springframework.boot.test.context.SpringBootTest;
3import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
4import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
5import org.springframework.test.context.junit4.SpringRunner;
6
7@RunWith(SpringRunner.class)
8@SpringBootTest
9@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL,
10 ids = "com.testwithspring.master.webinar:contract-rest-service:0.0.1-SNAPSHOT:stubs:8080"
11)
12public class TaskContractTest {
13
14}
Second, we have to add a RestTemplate field to our test class and implement a setup method that creates a new RestTemplate object and stores this object in the restTemplate field.
After we have created the RestTemplate that is used by our test methods, the source code of our test class looks as follows:
1import org.junit.Before;
2import org.junit.runner.RunWith;
3import org.springframework.boot.test.context.SpringBootTest;
4import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
5import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
6import org.springframework.test.context.junit4.SpringRunner;
7import org.springframework.web.client.RestTemplate;
8
9@RunWith(SpringRunner.class)
10@SpringBootTest
11@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL,
12 ids = "com.testwithspring.master.webinar:contract-rest-service:0.0.1-SNAPSHOT:stubs:8080"
13)
14public class TaskContractTest {
15
16 private RestTemplate restTemplate;
17
18 @Before
19 public void setup() {
20 restTemplate = new RestTemplate();
21 }
22}
Third, we have to ensure that our contract returns the correct HTTP response when we send a GET request to the URL that triggers our contract (http://localhost:8080/api/task/1). We can do this by following these steps:
- Ensure that our contract returns the HTTP status code 200.
- Verify that our contract returns a task that has the correct
id. - Ensure that our contract returns a task that has the correct
title.
After we have written the required test methods, the source code of our test class looks as follows:
1import org.junit.Before;
2import org.junit.Test;
3import org.junit.runner.RunWith;
4import org.springframework.boot.test.context.SpringBootTest;
5import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
6import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
7import org.springframework.http.HttpStatus;
8import org.springframework.http.ResponseEntity;
9import org.springframework.test.context.junit4.SpringRunner;
10import org.springframework.web.client.RestTemplate;
11
12import static org.assertj.core.api.Assertions.assertThat;
13
14@RunWith(SpringRunner.class)
15@SpringBootTest
16@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL,
17 ids = "com.testwithspring.master.webinar:contract-rest-service:0.0.1-SNAPSHOT:stubs:8080"
18)
19public class TaskContractTest {
20
21 private RestTemplate restTemplate;
22
23 @Before
24 public void setup() {
25 restTemplate = new RestTemplate();
26 }
27
28 @Test
29 public void shouldReturnHttpStatusCodeOk() {
30 ResponseEntity<Task> taskResponse = restTemplate.getForEntity(
31 "http://localhost:8080/api/task/1",
32 Task.class
33 );
34 assertThat(taskResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
35 }
36
37 @Test
38 public void shouldReturnTaskWithCorrectId() {
39 ResponseEntity<Task> taskResponse = restTemplate.getForEntity(
40 "http://localhost:8080/api/task/1",
41 Task.class
42 );
43
44 Task task = taskResponse.getBody();
45 assertThat(task.getId()).isEqualByComparingTo(1L);
46 }
47
48 @Test
49 public void shouldReturnTaskWithCorrectTitle() {
50 ResponseEntity<Task> taskResponse = restTemplate.getForEntity(
51 "http://localhost:8080/api/task/1",
52 Task.class
53 );
54
55 Task task = taskResponse.getBody();
56 assertThat(task.getTitle()).isEqualTo("Write our first contract");
57 }
58}
When we run our tests, we see that they pass. We can now verify that the contract provided by a REST producer hasn’t changed. However, this test doesn’t ensure that our REST consumer service is working as expected. Let’s move on and find out how we can write tests for our REST consumer service.
Writing Tests for Our REST Consumer Service
When we want to ensure that our REST consumer service is working as expected, we have to follow these steps:
First, we have to create a new test class and configure the system under test by following these steps:
- Ensure that our tests are run by the
SpringRunnerclass. - Enable the testing enhancements provided by Spring Boot.
- Enable the auto-configuration of the Spring MVC Test framework.
- Ensure that our test methods use the WireMock stubs generated by our REST producer Maven project. When we do this, we have to ensure that these stubs are fetched from the local Maven repository. Also, we have to ensure that these stubs listen to the port 8080.
After we have configured the system under test, the source code of our test class looks as follows:
1import org.junit.runner.RunWith;
2import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
3import org.springframework.boot.test.context.SpringBootTest;
4import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
5import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
6import org.springframework.test.context.junit4.SpringRunner;
7
8@RunWith(SpringRunner.class)
9@SpringBootTest
10@AutoConfigureMockMvc
11@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL,
12 ids = "com.testwithspring.master.webinar:contract-rest-service:0.0.1-SNAPSHOT:stubs:8080"
13)
14public class FindTaskByIdWhenTaskIsFoundTest {
15
16}
Second, we have to add a MockMvc field to our test class and ensure that the Spring container injects the real MockMvc object into this field.
After we have added this field to our test class, its source code looks as follows:
1import org.junit.runner.RunWith;
2import org.springframework.beans.factory.annotation.Autowired;
3import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
4import org.springframework.boot.test.context.SpringBootTest;
5import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
6import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
7import org.springframework.test.context.junit4.SpringRunner;
8import org.springframework.test.web.servlet.MockMvc;
9
10@RunWith(SpringRunner.class)
11@SpringBootTest
12@AutoConfigureMockMvc
13@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL,
14 ids = "com.testwithspring.master.webinar:contract-rest-service:0.0.1-SNAPSHOT:stubs:8080"
15)
16public class FindTaskByIdWhenTaskIsFoundTest {
17
18 @Autowired
19 private MockMvc mockMvc;
20}
Third, we have to ensure that our REST producer service is working as expected when the requested task is found. We can write these tests by following these steps:
- Ensure that our REST producer service returns the HTTP status code 200.
- Verify that our REST producer service returns the information of the found task as JSON.
- Ensure that our REST producer service returns the correct information.
After we have written these tests, the source code of our test class looks as follows:
1import org.junit.Test;
2import org.junit.runner.RunWith;
3import org.springframework.beans.factory.annotation.Autowired;
4import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
5import org.springframework.boot.test.context.SpringBootTest;
6import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
7import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
8import org.springframework.http.MediaType;
9import org.springframework.test.context.junit4.SpringRunner;
10import org.springframework.test.web.servlet.MockMvc;
11
12import static org.hamcrest.Matchers.is;
13import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
14import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
15import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
16import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
17
18@RunWith(SpringRunner.class)
19@SpringBootTest
20@AutoConfigureMockMvc
21@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL,
22 ids = "com.testwithspring.master.webinar:contract-rest-service:0.0.1-SNAPSHOT:stubs:8080"
23)
24public class FindTaskByIdWhenTaskIsFoundTest {
25
26 @Autowired
27 private MockMvc mockMvc;
28
29 @Test
30 public void shouldReturnHttpStatusCodeOk() throws Exception {
31 mockMvc.perform(get("/api/task/{id}", 1L))
32 .andExpect(status().isOk());
33 }
34
35 @Test
36 public void shouldReturnFoundTaskAsJsonOk() throws Exception {
37 mockMvc.perform(get("/api/task/{id}", 1L))
38 .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8));
39 }
40
41 @Test
42 public void shouldReturnInformationOfCorrectTask() throws Exception {
43 mockMvc.perform(get("/api/task/{id}", 1L))
44 .andExpect(jsonPath("$.id", is(1)))
45 .andExpect(jsonPath("$.title", is("Write our first contract")));
46 }
47}
When we run our tests, we see that they pass. We can now implement a REST consumer service and ensure that our REST consumer service is working as expected. Let’s summarize what we learned from this webinar.
Summary
This webinar has taught us four things:
- The Spring Cloud Contract Stub Runner can download and run the WireMock stubs generated by a REST producer service Maven project.
- Before we can write these tests, we have to generate the WireMock stubs which “implement” our consumer-driven contracts and ensure that these stubs can be used by our integration tests.
- We can configure the started stubs by using the
@AutoConfigureStubRunnerannotation. - When we write tests, we can both ensure that the contract provided by our REST producer service hasn’t changed and verify that our REST consumer service is working as expected.