DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • How Kafka Can Make Microservice Planet a Better Place
  • Spring Cloud Stream Binding Kafka With EmbeddedKafkaRule Using In Tests
  • Testing Spring Boot Apps With Kafka and Awaitility
  • WireMock: The Ridiculously Easy Way (For Spring Microservices)

Trending

  • Operational Principles, Architecture, Benefits, and Limitations of Artificial Intelligence Large Language Models
  • Using Python Libraries in Java
  • Next Evolution in Integration: Architecting With Intent Using Model Context Protocol
  • Building Reliable LLM-Powered Microservices With Kubernetes on AWS
  1. DZone
  2. Data Engineering
  3. Big Data
  4. Consumer-Driven Contract Testing With Spring Cloud Contract

Consumer-Driven Contract Testing With Spring Cloud Contract

Implement CDC testing with Spring Cloud Contract and two sample Spring Boot applications to understand the communication between microservices.

By 
Jagdish Raika user avatar
Jagdish Raika
·
Updated Oct. 15, 20 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
15.6K Views

Join the DZone community and get the full member experience.

Join For Free

contact testing

Introduction

The article demonstrates how to write a contract between the producer & the consumer and how to implements the producer and the consumer side test cases for Spring Cloud Contract through an HTTP request between two microservices.

Producer/Provider

The producer is a service that exposes an API (e.g. rest endpoint) or sends a message (e.g. Kafka Producer which publishes the message to Kafka Topic)

Consumer

The consumer is a service that consumes the API that is exposed by the producer or listens to a message from the producer (e.g. Kafka Consumer which consumes the message from Kafka Topic)

Contract

The contract is an agreement between the producer and consumer how the API/message will look like.

  • What endpoints can we use?
  • What input do the endpoints take?
  • What does the output look like?

Consumer-Driven Contract 

Consumer-driven contract (CDD) is an approach where the consumer drives the changes in the API of the producer. 

Consumer-driven contract testing is an approach to formalize above mentioned expectations into a contract between each consumer-provider pair. Once the contract is established between Provider and Consumer, this ensures that the contract will not break suddenly.

Spring Cloud Contract

Spring Cloud Contract is a project of spring-cloud that helps end-users in successfully implementing the Consumer Driven Contracts (CDC) approach. The Spring Cloud Contract Verifier is used as a tool that enables the development of Consumer Driven Contracts. Spring Cloud Contract Verifier is used with Contract Definition Language (DSL) written in Groovy or YAML.

Demo Application

To understand the concept of the Spring Cloud Contract, I have implemented two simple microservices. The code for these applications can be found on GitHub account.

request and response

Request and response between the consumer and the producer

Create-employee-application MS

It is the first microservice responsible for creating an employee's profile based on the given details. We are only passing the FirstName, LastName, and Identification Number (e.g. National ID) of the employee. This microservice is calling another microservice to first check, based on the Identity Number, whether the profile has already been created for the employee.

Get-employee-application MS

This is the second microservice service that is just checking if an employee profile already exists. If the employee profile is matching with the Identification Number provided in the database, it will return the profile else return an empty profile with the EMPLOYEE_NOT_FOUND status.

The create-employee-application microservice is having a dependency on get-employee-application microservice, so we have written a contract of get-employee-application. We are not using any database here to store or retrieve employee details so that written simple logic which will help us to fetch the existing employee profile.

Setup

We are going to understand how we have done the setup for these applications. We are going to discuss the setup in each microservice one by one.

Provider Side Setup of Get-Employee-Application

Maven Dependencies and Plugin

We are supposed to add the Spring Cloud Contract Verifier dependency and plugin to our pom.xml, as the following example shows:

XML
 




xxxxxxxxxx
1
18


 
1
 <dependency>
2
    <groupId>org.springframework.cloud</groupId>
3
    <artifactId>spring-cloud-starter-contract-verifier</artifactId>
4
    <version>${spring-cloud-contract.version} </version>
5
    <scope>test</scope>
6
</dependency>
7

          
8
<plugin>
9
    <groupId>org.springframework.cloud</groupId>
10
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
11
    <version>>${spring-cloud-contract.version} </version>
12
    <extensions>true</extensions>
13
    <configuration>
14
        <baseClassForTests>
15
            com.jd.spring.cloud.contract.get.BaseClass
16
        </baseClassForTests>
17
    </configuration>
18
</plugin>



BaseClass Setup

We need to use this BaseClass as the base class of the generated test. We should configure this class in the spring-cloud-contract-maven-plugin shown as above.

Java
 




xxxxxxxxxx
1
25


 
1
public abstract class BaseClass {
2

          
3
    @LocalServerPort
4
    private int port;
5

          
6
    @Value("${app.employeeBaseURI:http://localhost}")
7
    String employeeBaseURI;
8

          
9
    @Value("${app.employeeBasePath:/employee-management/employee}")
10
    String employeeBasePath;
11

          
12
    @Before
13
    public void setup() {
14
        RestAssured.useRelaxedHTTPSValidation();
15
        RestAssured.baseURI = employeeBaseURI;
16
        if (RestAssured.baseURI.contains("localhost")) {
17
            RestAssured.port = port;
18
        }
19
    }
20

          
21
    public String getUrlPath() {
22
        return employeeBasePath;
23
    }
24
}



GetEmployeeController

This is the controller being used to return employee profiles based on the given identification number.

Java
 




xxxxxxxxxx
1
21


 
1
@RestController
2
public class GetEmployeeController {
3

          
4
   @Autowired
5
   private DBRepository dbRepository;
6

          
7
   @RequestMapping(value="/employee/{identityCardNo}", method = RequestMethod.GET)
8
   public ResponseEntity<Employee> getEmployee(@PathVariable String identityCardNo)
9
   {
10
      if (!identityCardNo.startsWith("0")){
11
         return new ResponseEntity<Employee>(dbRepository.getEmployee(identityCardNo), HttpStatus.OK);
12
      }
13

          
14
      Employee employee = new Employee();
15
      employee.setStatus("EMPLOYEE_NOT_FOUND");
16

          
17
      return new ResponseEntity<Employee>(employee, HttpStatus.OK);
18

          
19
   }
20
}



Contracts

There are two scenarios that we are going to cover here so two contracted groovy files are written. The contract groovy files are located under src/test/resources/contracts.employee directory where contracts.employee is the name of the package mentioned in the groovy files. 

Scenario 1 (shouldReturnExistingEmployee.groovy):

Groovy
 




xxxxxxxxxx
1
39


 
1
package contracts.employee
2
import org.springframework.cloud.contract.spec.Contract
3

          
4
Contract.make {
5
    description "should return an employee profile for given details."
6
    request {
7
        method(GET())
8
        urlPath("/employee-management/employee/")
9
        urlPath($(
10
                consumer(regex("/employee-management/employee/[1-9][0-9]{0,}"))
11
                , producer("/employee-management/employee/1234567890")
12
        ))
13

          
14
        headers {
15
            contentType(applicationJson())
16
            accept(applicationJson())
17
        }
18
    }
19

          
20
    response {
21
        status OK()
22
        headers {
23
            contentType applicationJson()
24
        }
25
        body(
26
                "id": "${(regex('[1-9][0-9]{0,}'))}",
27
                "firstName": anyAlphaUnicode(),
28
                "lastName": anyAlphaUnicode(),
29
                "identityCardNo": fromRequest().path(2),
30
                "status": "EMPLOYEE_FOUND"
31
        )
32
    }
33
}


 

Scenario 2 (shouldReturnNotFoundEmployee.groovy):

Groovy
 




xxxxxxxxxx
1
26


 
1
package contracts.employee
2
import org.springframework.cloud.contract.spec.Contract
3

          
4
Contract.make {
5
    description "should return an empty employee profile for given details"
6
    request {
7
        method(GET())
8
        urlPath("/employee-management/employee/")
9
        urlPath($(
10
                consumer(regex("/employee-management/employee/[0][0-9]{0,}"))
11
                , producer("/employee-management/employee/0123456789")
12
        ))
13
        headers {
14
            contentType(applicationJson())
15
            accept(applicationJson())
16
        }
17
    }
18
    response {
19
        status OK()
20
        headers {
21
            contentType applicationJson()
22
        }
23
        body("status": "EMPLOYEE_NOT_FOUND")
24
    }
25
}
26

          



Consumer Side Setup of Create-Employee-Application

Maven Dependency

To run the stubs, we need to add below dependency on the consumer side of create-employee-application.

XML
 




x


 
1
<dependency>
2
    <groupId>org.springframework.cloud</groupId>
3
    <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
4
     <version>${spring-cloud-contract.version} </version>
5
    <scope>test</scope>
6
</dependency>



CreateEmployeeController

This is the controller being used to create employee profiles based on the given details of the employee.

Java
 




xxxxxxxxxx
1
29


 
1
@RestController
2
public class CreateEmployeeController {
3

          
4
    @Value("${location.getEmployee.url:http://localhost:8180/employee-management}")
5
    private String location;
6

          
7
    @Autowired
8
    private DBRepository dbRepository;
9

          
10
    @RequestMapping(value= "/employee", method = RequestMethod.POST)
11
    public ResponseEntity<Employee> createEmployee(@RequestBody Employee employee)
12
    {
13
        RestTemplate restTemplate = new RestTemplate();
14
        HttpHeaders headers=new HttpHeaders();
15
        headers.add("Content-Type","application/json");
16
        headers.add("accept","application/json");
17

          
18
        ResponseEntity<Employee> response = restTemplate
19
                .exchange(location+"/employee/"+employee.getIdentityCardNo(), GET, new HttpEntity<>(headers), Employee.class);
20

          
21
        if("EMPLOYEE_FOUND".equals(response.getBody().getStatus())){
22

          
23
            return response;
24
        }
25

          
26
        return new ResponseEntity<Employee>(dbRepository.createEmployee(employee), HttpStatus.CREATED);
27

          
28
    }
29
}



Test Class

This test is consuming the get-employee-test-provider-contract stub and running the mock service on 8180 port. There are two tests in this class that is covering both scenarios written in the contract of get-employee-application.

We are using the StubsMode as StubRunnerProperties.StubsMode.LOCAL. We can control the stub downloading via the stubsMode switch. The following options can be used to download the stub:-

  • StubRunnerProperties.StubsMode.CLASSPATH (default value) - This is the default mode. It will scan the classpath and pick stubs from there. We need to add the dependency of the stub with classifier as a stub in the pom.xml with test scope.
  • StubRunnerProperties.StubsMode.LOCAL - It will pick stubs from a local m2 repository.
  • StubRunnerProperties.StubsMode.REMOTE - It will pick stubs from a remote location e.g. Nexus. We need to initialize repositoryRoot property with the URL of the remote repository in the AutoConfigureStubRunner annotation.
Java
 




xxxxxxxxxx
1
86


 
1
@RunWith(SpringRunner.class)
2
@SpringBootTest(
3
        classes = CreateEmployeeApplication.class,
4
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
5
        )
6
@AutoConfigureStubRunner(
7
        ids = {"com.jd.spring:get-employee-test-provider-contract:8180"}
8
        , stubsMode = StubRunnerProperties.StubsMode.LOCAL)
9
public class ConsumeGetEmployeeUsingStubTest {
10

          
11
    @LocalServerPort
12
    private int port;
13

          
14
    @Value("${app.createEmployeeBaseURI:http://localhost}")
15
    String createEmployeeBaseURI;
16

          
17
    @Value("${app.createEmployeeBasePath:/employee-management/employee}")
18
    String createEmployeeBasePath;
19

          
20
    @Before
21
    public void setup() {
22

          
23
        RestAssured.useRelaxedHTTPSValidation();
24
        RestAssured.baseURI = createEmployeeBaseURI;
25
        if (RestAssured.baseURI.contains("localhost")) {
26
            RestAssured.port = port;
27
        }
28
    }
29

          
30
    @Test
31
    public void testShouldCreateNewEmployee() throws Exception {
32
        // given:
33
        RequestSpecification request = given()
34
                .header("Content-Type", "application/json")
35
                .header("Accept", "application/json")
36
                .body(
37
                        "{\"firstName\":\"Jagdish\",\"lastName\":\"Raika\",\"identityCardNo\":\"0123456789\"}");
38

          
39
        // when:
40
        Response response = given().spec(request)
41
                .post(createEmployeeBasePath);
42

          
43
        // then:
44
        assertThat(response.statusCode()).isEqualTo(201);
45
        assertThat(response.header("Content-Type")).matches("application/json.*");
46

          
47
        System.out.println("testShouldCreateNewEmployee: "+response.getBody().asString());
48

          
49
        // and:
50
        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
51
        assertThatJson(parsedJson).field("['id']").matches("[1-9][0-9]{0,}");
52
        assertThatJson(parsedJson).field("['firstName']").matches("Jagdish");
53
        assertThatJson(parsedJson).field("['lastName']").matches("Raika");
54
        assertThatJson(parsedJson).field("['identityCardNo']").isEqualTo("0123456789");
55
        assertThatJson(parsedJson).field("['status']").matches("NEW_EMPLOYEE_CREATED");
56
    }
57

          
58
    @Test
59
    public void testShouldReturnExistingEmployee() throws Exception {
60
        // given:
61
        RequestSpecification request = given()
62
                .header("Content-Type", "application/json")
63
                .header("Accept", "application/json")
64
                .body(
65
                        "{\"firstName\":\"Jagdish\",\"lastName\":\"Raika\",\"identityCardNo\":\"1234567890\"}");
66

          
67
        // when:
68
        Response response = given().spec(request)
69
                .post(createEmployeeBasePath);
70

          
71
        // then:
72
        assertThat(response.statusCode()).isEqualTo(200);
73
        assertThat(response.header("Content-Type")).matches("application/json.*");
74

          
75
        System.out.println("testShouldReturnExistingEmployee: "+response.getBody().asString());
76

          
77
        // and:
78
        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
79
        assertThatJson(parsedJson).field("['id']").matches("[1-9][0-9]{0,}");
80
        assertThatJson(parsedJson).field("['firstName']").matches("[\\p{L}]*");
81
        assertThatJson(parsedJson).field("['lastName']").matches("[\\p{L}]*");
82
        assertThatJson(parsedJson).field("['identityCardNo']").isEqualTo("1234567890");
83
        assertThatJson(parsedJson).field("['status']").matches("EMPLOYEE_FOUND");
84
    }
85
}



Generated Source and Stubs at the Provider Side

When we trigger the build of the provider, the spring-cloud-contract-maven-plugin automatically generates some test classes and stubs by using contract groovy files. Let's have a look at the directory structure of the provider project after triggering the build.

Project Structure of the provider

Project structure of the provider

Generated Test

When we trigger the build, the spring-cloud-contract-maven-plugin automatically generates a test class named EmployeeTest that extends our BaseClass and puts it in /target/generated-test-sources/contracts. 

The name of the test class will be on the basis of the last keyword of the groovy package (employee) and it will be appended with the 'test' keyword so the full name of the generated class will be 'EmployeeTest'. The name of the tests under this class will be starting with the keyword 'validate' appended with '_' and the name of the groovy file.

Example:

  • Validate_shouldReturnExistingEmployee
  • Validate_shouldReturnNotFoundEmployee
Java
 




xxxxxxxxxx
1
47


 
1
@SuppressWarnings("rawtypes")
2
public class EmployeeTest extends BaseClass {
3

          
4
    @Test
5
    public void validate_shouldReturnExistingEmployee() throws Exception {
6
        // given:
7
            RequestSpecification request = given()
8
                    .header("Content-Type", "application/json")
9
                    .header("Accept", "application/json");
10

          
11
        // when:
12
            Response response = given().spec(request)
13
                    .get("/employee-management/employee/1234567890");
14

          
15
        // then:
16
            assertThat(response.statusCode()).isEqualTo(200);
17
            assertThat(response.header("Content-Type")).matches("application/json.*");
18

          
19
        // and:
20
            DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
21
            assertThatJson(parsedJson).field("['id']").matches("[1-9][0-9]{0,}");
22
            assertThatJson(parsedJson).field("['firstName']").matches("[\\p{L}]*");
23
            assertThatJson(parsedJson).field("['lastName']").matches("[\\p{L}]*");
24
            assertThatJson(parsedJson).field("['identityCardNo']").isEqualTo("1234567890");
25
            assertThatJson(parsedJson).field("['status']").isEqualTo("EMPLOYEE_FOUND");
26
    }
27

          
28
    @Test
29
    public void validate_shouldReturnNotFoundEmployee() throws Exception {
30
        // given:
31
            RequestSpecification request = given()
32
                    .header("Content-Type", "application/json")
33
                    .header("Accept", "application/json");
34

          
35
        // when:
36
            Response response = given().spec(request)
37
                    .get("/employee-management/employee/0123456789");
38

          
39
        // then:
40
            assertThat(response.statusCode()).isEqualTo(200);
41
            assertThat(response.header("Content-Type")).matches("application/json.*");
42

          
43
        // and:
44
            DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
45
            assertThatJson(parsedJson).field("['status']").isEqualTo("EMPLOYEE_NOT_FOUND");
46
    }
47
}



Generated Stubs

When we trigger the build, the spring-cloud-contract-maven-plugin automatically generates a stub inside the target/stubs directory. The stub is the generated WireMock of the contracts that will be converted into JSON format by the spring-cloud-contract-maven-plugin. We can refer above image for the directory structure of the provider project. This stub is being used to mock the provider side.

This stubs will be packed inside a jar in the package phase of the maven with a classifier as stubs.

How the Contract Will Be Broken

If we are making any changes on the provider side without updating or informing the consumer side, it will take us to a result where the contract will be broken and the test will fail.

Let's take an example, we are making some changes in the controller side such as changing the URL to fetch the employee profile. If we do not inform the consumer, they will keep trying on the old URL and they will get 404 Not Found instead of the desired response and this will lead to the failure of the contract.

Source Code

The source code for this post can be found on the GitHub account.

Spring Cloud Spring Framework kafka Stub (distributed computing) microservice Testing Profile (engineering) Groovy (programming language)

Opinions expressed by DZone contributors are their own.

Related

  • How Kafka Can Make Microservice Planet a Better Place
  • Spring Cloud Stream Binding Kafka With EmbeddedKafkaRule Using In Tests
  • Testing Spring Boot Apps With Kafka and Awaitility
  • WireMock: The Ridiculously Easy Way (For Spring Microservices)

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!