Introduction to Spring Cloud Contract, Part One
After we have finished this webinar, we:
- Are familiar with the basic concepts of the Consumer-Driven Contracts approach.
- Can identify the key features of Spring Cloud Contract.
- Can create a Maven project for a REST producer service.
- Can create our first consumer driven contract.
- Understand how we can create a REST producer service that fulfills our contract.
Let’s start by taking a quick look at consumer-driven contracts.
Get the source code from Github
A Very Quick Introduction to Consumer-Driven Contracts
The consumer-driven contracts approach helps us to write applications which use the microservices architecture. When we are writing an application by using the consumer-driven contracts approach, we have to implement the following components:
- A provider is a component that provides a public API which can be consumed by other components.
- A consumer is a components that consumes the API provided by a provider.
- A contract specifies an interaction between a consumer and a provider. To be more specific, it specifies the response that should be returned by the provider when it receives the request specified by the contract.
Next, we will take a quick look at the key features of Spring Cloud Contract.
The Key Features of Spring Cloud Contract
The Spring Cloud Contract project integrates different technologies with each other, and its goal is to help us to write applications by using consumer-driven contracts. Its key features are:
- We can create contracts by using Groovy DSL or Yaml.
- Spring Cloud Contract generates test classes which help us to ensure that our producers honor the created contracts.
- Spring Cloud Contract generates stubs which provide test data that allows us to write tests for our consumers. In other words, these stubs help us to ensure that our consumers can consume the data returned by our producers.
Let’s move on and find out how we can create a Maven project for a REST producer service.
Creating a Maven Project for a REST Producer Service
Because our example application uses Spring Boot, our Maven project is pretty much a normal Spring Boot project. However, there are three things I want point out:
First, we have to add the Spring Cloud Contract BOM to our Maven build. This BOM specifies the required dependencies of Spring Cloud Contract. We can do this by adding 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 create contracts and generate test classes automatically, we have to declare the spring-cloud-starter-contract-verifier dependency in our POM file. We can do this 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-verifier</artifactId>
4 <scope>test</scope>
5</dependency>
Third, we have configure the Spring Cloud Contract Maven plugin. This plugin generates tests from the provided contracts and creates stubs that can be used to test the consumer which consumes our REST API. We can configure this plugin by following these steps:
- Add the Spring Cloud Contract Maven Plugin to our build.
- Ensure that this plugin can add the required libraries to the core class loader. This is a crucial step. If we don’t do this, the Spring Cloud Contract Maven Plugin cannot generate test classes or create the required stubs.
- Configure the fully qualified class name of the base class that is extended by the generated test classes.
After we have configured the Spring Cloud Contract Maven plugin, its configuration looks as follows:
1<plugin>
2 <groupId>org.springframework.cloud</groupId>
3 <artifactId>spring-cloud-contract-maven-plugin</artifactId>
4 <version>2.0.1.RELEASE</version>
5 <extensions>true</extensions>
6 <configuration>
7 <baseClassForTests>com.testwithspring.master.webinar.BaseTest</baseClassForTests>
8 </configuration>
9</plugin>
Additional Reading:
We have now created a Maven project that can compile and run our REST product service. Next, we will implement our REST product service.
Implementing a REST Producer Service
When we want to implement a REST API that returns the information of a single task, we have to follow these steps:
First, we have to create a class that contains the information of the returned 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 implement a service that returns the information of the requested task. Because this is just an example, we can write this service by following these steps:
- Add a
Mapfield to our service class. This field contains the tasks returned by our service. - Implement a constructor that initializes this field and adds two tasks to our map.
- Write a
findById()method that gets the requested task from our map.
After we have written our service class, its source code looks as follows:
1import org.springframework.stereotype.Service;
2
3import java.util.HashMap;
4import java.util.Map;
5
6@Service
7class TaskService {
8
9 private final Map<Long, Task> tasks;
10
11 TaskService() {
12 tasks = new HashMap<>();
13 tasks.put(1L, new Task(1L, "Write REST service"));
14 tasks.put(2L, new Task(2L, "Write REST client"));
15 }
16
17 Task findById(Long id) {
18 return tasks.get(id);
19 }
20}
Third, before we will write the controller that implements our REST API, we have to create the contract that specifies its behavior. When we create this contract, we have to follow these two rules:
- We have to put our contracts to the src/test/resources/contracts directory.
- We can specify our contracts by using either Groovy DSL or Yaml.
We can now create our contract by following these steps:
- Create a new contract by calling the
static make()method of theContractclass. - Provide a description for our contract.
- Identify the HTTP request that triggers our contract. In our case, our contract is valid when our REST API receives a
GETrequest to the path: ‘/api/task/1’. - Specify the HTTP response that is returned by our REST API. In this case, our REST API must return the information of the found task as JSON.
After we have written our contract (find_task_by_id.groovy) by using Groovy, it looks as follows:
1import org.springframework.cloud.contract.spec.Contract
2
3Contract.make {
4 description 'should return a task when the id of the requested task is 1'
5
6 request {
7 url '/api/task/1'
8 method GET()
9 }
10
11 response {
12 status 200
13 headers {
14 contentType applicationJson()
15 }
16 body (
17 id: 1,
18 title: 'Write our first contract'
19 )
20 }
21}
Additional Reading:
When we compile our project, the Spring Cloud Contract Maven plugin creates a test class which tests our REST API by using Rest Assured. This test class extends the BaseTest class, and its source code looks as follows:
1import com.jayway.jsonpath.DocumentContext;
2import com.jayway.jsonpath.JsonPath;
3import com.testwithspring.master.webinar.BaseClass;
4import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification;
5import io.restassured.response.ResponseOptions;
6import org.junit.Test;
7
8import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
9import static io.restassured.module.mockmvc.RestAssuredMockMvc.*;
10import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
11
12public class WebinarTest extends BaseTest {
13
14 @Test
15 public void validate_find_task_by_id() throws Exception {
16 // given:
17 MockMvcRequestSpecification request = given();
18
19 // when:
20 ResponseOptions response = given().spec(request)
21 .get("/api/task/1");
22
23 // then:
24 assertThat(response.statusCode()).isEqualTo(200);
25 assertThat(response.header("Content-Type"))
26 .matches("application/json.*");
27 // and:
28 DocumentContext parsedJson = JsonPath.parse(
29 response.getBody().asString()
30 );
31 assertThatJson(parsedJson).field("['id']")
32 .isEqualTo(1);
33 assertThatJson(parsedJson).field("['title']")
34 .isEqualTo("Write our first contract");
35 }
36}
Fourth, we have to write the controller class that processes GET requests send to the path: ‘/api/task/{id}’. This controller gets the returned task by invoking the findById() method of the TaskService class.
After we have written our controller class, its source code looks as follows:
1import org.springframework.web.bind.annotation.GetMapping;
2import org.springframework.web.bind.annotation.PathVariable;
3import org.springframework.web.bind.annotation.RestController;
4
5@RestController
6class TaskController {
7
8 private final TaskService service;
9
10 TaskController(TaskService service) {
11 this.service = service;
12 }
13
14 @GetMapping("/api/task/{id}")
15 Task findById(@PathVariable("id") Long id) {
16 return service.findById(id);
17 }
18}
Fifth, we have to write an abstract BaseTest class which configures the system under test by using a setup method that is run before a test method is run. We can implement this setup method by following these steps:
- Create a new
TaskServicemock. - Create a new
MockMvcobject by using the standalone configuration. We must remember to ensure that the created object supports REST Assured. Also, we must register the tested controller. - Ensure that our mock object returns the information of the found task when it is invoked by using the method parameter
1L.
After we have written our setup method, the source code of the BaseTest class looks as follows:
1import io.restassured.module.mockmvc.RestAssuredMockMvc;
2import org.junit.Before;
3import org.junit.runner.RunWith;
4import org.springframework.beans.factory.annotation.Autowired;
5import org.springframework.boot.test.context.SpringBootTest;
6import org.springframework.boot.test.mock.mockito.MockBean;
7import org.springframework.test.context.junit4.SpringRunner;
8
9import static org.mockito.BDDMockito.given;
10import static org.mockito.Mockito.mock;
11
12public abstract class BaseTest {
13
14 @Before
15 public void setup() {
16 TaskService service = mock(TaskService.class);
17
18 RestAssuredMockMvc.standaloneSetup(new TaskController(service));
19 given(service.findById(1L))
20 .willReturn(new Task(1L, "Write our first contract"));
21 }
22}
When we run our tests, we see that they pass. We have now written a producer service that fulfills our contract. Let’s summarize what we learned from this webinar.
Summary
This webinar has taught five things:
- Contracts specify the interactions between a producer and a consumer.
- We can create contracts by using Gradle DSL or Yaml.
- The Spring Cloud Contract Maven Plugin generates both test classes and producer stubs.
- The tests classes generated by the Spring Cloud Contract Maven Plugin extend a base class that must be implemented by us.
- The base class can use the standalone configuration or it can load the Spring application context. In other words, we can write either unit or integration tests for our producer.