Using Spring Cloud Contract for Consumer Driven Contracts

Using Spring Cloud Contract for Consumer Driven Contracts

2 Comments

Last Updated on June 3, 2019 by Simanta

Consumer Driven Contracts are considered a design pattern for evolving services. Spring Cloud Contract can be used to implement consumer driven contracts for services developed using the Spring Framework.

In this post, I’ll take an in-depth look at using Spring Cloud Contract to create Consumer Driven Contracts.

An Overview of Spring Cloud Contract

Let’s consider a typical scenario where a consumer sends a request to a producer.

If you were working on the API consumer, how can you write a test without creating a dependency on the API producer?

One tool you can use is WireMock. In WireMock, you define stubs in JSON and then test your consumer calls against the stubs.

By using a tool like WireMock, when developing the consumer side code, you break any dependency on the producer implementation.

Here is an example of WireMock stub.

{
  "id" : "b0cf8628-b85c-48fe-9584-cfda1a197fc7",
  "request" : {
    "urlPath" : "/gamemanager",
    "method" : "POST",
    "headers" : {
      "Content-Type" : {
        "matches" : "application/json.*"
      }
    },
    "queryParameters" : {
      "game" : {
        "equalTo" : "football"
      }
    },
    "bodyPatterns" : [ {
      "matchesJsonPath" : "$[?(@.['score'] =~ /[5,9][0,9][0,9]/)]"
    }, {
      "matchesJsonPath" : "$[?(@.['name'] == 'Tim')]"
    } ]
  },
  "response" : {
    "status" : 200,
    "body" : "{\"result\":\"ELIGIBLE\"}",
    "headers" : {
      "Content-Type" : "application/json"
    },
    "transformers" : [ "response-template" ]
  },
  "uuid" : "b0cf8628-b85c-48fe-9584-cfda1a197fc7"
}

This WireMock stub definition says that given a request of method POST sent to URL gamemanagerwith a query parameter game and a request body, we would like a response of status 200 with JSON in the body.

Spring Cloud Contract allows you to write a contract for a service using a Groovy or YAML DSL. This contract sets up the communication rules, such as the endpoint address, expected requests, and responses for both the service provider and consumer.

Spring Cloud Contract provides plugins for Maven and Gradle to generate stubs from contracts. These are WireMock stubs like the example above.

The consumer side of the application can then use the stubs for testing.

For the producer side, Spring Cloud Contract can generate unit tests which utilize Spring’s MockMVC.

It’s important to maintain the distinction between consumer and producers. Spring Cloud Contract is producing testing artifacts which allow the development of the consumer side code and the producer side code to occur completely independently.

This independence is very important. On small projects, it’s fairly common for a developer or small team to have access to the course code for both consumer and producer.

On larger projects, or when dealing with third parties, often you will not have access to both sides of the API.

For example, you may be a Spring developer providing an API for the UI team to write against.  By providing WireMock stubs the UI team can run the stubs for their development. The client-side technology does not need to be Spring or even Java. They could be using Angular or ReactJS.

The WireMock stubs can be run in a stand-alone server, allowing any RESTful client to interact with the defined stubs.

Let’s take a closer look at the process by setting up a Producer and Consumer.

The Initial Producer

Let’s say the Producer is in development.

Right now, the Producer does not have any implementation code but only the service contract in the form of Groovy DSL.

The Maven POM

Before we start writing the contracts, we need to configure Maven.

We need to include the spring-cloud-starter-contract-verifier as a Maven dependency.

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-contract-verifier</artifactId>
   <scope>test</scope>
</dependency>

We also need to add spring-cloud-dependencies in the section of our Maven POM.

<dependencyManagement>
   <dependencies>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-dependencies</artifactId>
         <version>${spring-cloud.version}</version>
         <type>pom</type>
         <scope>import</scope>
      </dependency>
   </dependencies>
</dependencyManagement>

Finally, Spring Cloud Contract provides a plugin for both Maven and Gradle that generates stubs from DSLs.

We need to include the plugin in our Maven POM.

<plugin>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-contract-maven-plugin</artifactId>
   <version>${spring-cloud-contract.version}</version>
   <extensions>true</extensions>
   <configuration>
      <baseClassForTests>guru.springframework.GameBaseClass</baseClassForTests>
   </configuration>
</plugin>

Here is the complete pom.xml of the Producer.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>guru.springframework</groupId>
    <artifactId>game-api-producer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>Producer</name>
    <description>Demo producer for Spring Consumer Diven Contract</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Camden.SR7</spring-cloud.version>
        <spring-cloud-contract.version>2.0.1.RELEASE</spring-cloud-contract.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-contract-verifier</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-contract-dependencies</artifactId>
                <version>2.0.1.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

            <plugin>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-contract-maven-plugin</artifactId>
                <version>${spring-cloud-contract.version}</version>
                <extensions>true</extensions>
                <configuration>
                    <baseClassForTests>guru.springframework.GameBaseClass</baseClassForTests>
                </configuration>
            </plugin>

        </plugins>
    </build>


</project>

 

The Spring Cloud Contract Groovy DSL

Spring Cloud Contract supports out of the box two types of DSLs: Groovy and YAML.

In this post, I will use Spring Cloud Contract Groovy DSL. DSL or Domain-specific language is meant to make the code written in Groovy human-readable.

Currently our Producer, the Game Manager is responsible for allowing a player with a score greater than 500 to play a game of football. Any player with a score of less than 500 is not allowed to play the game.

Our first contract defines a successful POST request to the Game Manager.

Here is the game_contract_for_score_greater_than_500.groovy DSL.

game_contract_for_score_greater_than_500.groovy
import org.springframework.cloud.contract.spec.Contract

Contract.make {
  name("game-contract-for-score-greater_than_500")
  description """
Represents a successful scenario for playing a game

```
given:
  player score is greater than 500
when:
  he wants to play football game
then:
  we wiull allow him to play
```

"""
  request {
    method 'POST'
    urlPath('/gamemanager') {
      queryParameters {
        parameter('game', 'football')
      }
    }
    body(
        name: 'Tim',
        score: 600
    )
    headers {
      contentType(applicationJson())
    }
  }

  response {
    status 200
    headers {
      contentType applicationJson()
    }
    body (
        result: "ELIGIBLE"
    )
  }
}

 

The other contract defines a POST request to the Game Manager for a score lesser than 500. The game_contract_for_score_lesser_than_500.groovy DSL is this.

game_contract_for_score_lesser_than_500.groovy
import org.springframework.cloud.contract.spec.Contract

Contract.make {
  name("game-contract-for-score-lesser_than_500")
  description """
Represents a successful scenario for playing a game

```
given:
  player score is less than 500
when:
  he wants to play football game
then:
  we'll not allow him to play
```

"""
  request {
    method 'POST'
    urlPath('/gamemanager') {
      queryParameters {
                parameter('game', 'football')
      }
    }
    body(
                name:'Tim',
        score: 300
    )
    headers {
      contentType(applicationJson())
    }
  }

  response {
    status 200
    headers {
      contentType applicationJson()
    }
    body (
      result: "NOT ELIGIBLE"
    )
  }
}

 

As you can see the DSLs are intuitive. The code imports the Contract class and calls the make method. The contract, in addition to the contract name and description, has two parts: request and response.

For this example, we have hardcoded the request values. However, Groovy DSL supports dynamic values through regular expressions. More information here.

The Consumer

The consumer will have a REST controller that receives requests for playing game. The consumer needs to verify with the Game Manager before starting the game by making a request to it.

On the consumer side, fork or clone the producer and run this command.

 mvn clean install –DskipTests

On running the command, the Spring Cloud Contract Maven plugin does two things:

  1. Converts the Groovy DSL contracts into a WireMock stub.
  2. Installs the JAR containing the stub into your Maven local repository

NOTE: In a typical enterprise situation where the consumer is a different team, the JAR artifact would typically be published to an internal maven repository.

This figure shows the generated stub in the Project window of IntelliJ.
Generated Stub

We are now ready to implement the consumer.

The Maven POM

One of the tool that the consumer needs is the stub runner, which is part of the Spring Cloud Contract project. Stub runner is responsible for starting up the producer stub on the consumer side. This enables the consumer to make requests to the stub offline and do all necessary testing before going live in production.

The code to include the stub runner dependency in the Maven POM is this.

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
   <version>2.0.1.RELEASE</version>
   <scope>test</scope>
</dependency>

We will also use Project Lombok, a Java library that auto-generates boilerplate code based on annotations you provide in your code. The Lombok dependency declaration in the Maven POM is this.

<dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.0</version>
      <scope>provided</scope>
</dependency>

The complete Maven POM on the consumer side is this.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>guru.springframework</groupId>
  <artifactId>game-api-consumer</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>Consumer</name>
  <description>Demo consumer for Spring Consumer Diven Contract</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.2.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>

  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
      <version>1.2.4.RELEASE</version>
      <scope>test</scope>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.0</version>
      <scope>provided</scope>
    </dependency>

  </dependencies>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

 

The Domain Object

We will model a player as a domain object on the consumer side.

Here is the code for the Player class.

Player.java
package guru.springframework.consumer.controller.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@Builder
@NoArgsConstructor
public class Player {
    private String name;

      private int score;
}

 

I’ve used Lombok annotations on the Player class. As you can see the class is much cleaner and only focuses on the properties it represents.

The @Data Lombok annotations will generate the getter, setter, toString(), hashCode(), and equals() method for us during the build.

The @AllArgsConstructor and @NoArgsConstructor will add a constructor to initialize all properties and a default constructor for us.

The @Builder annotation will add builder APIs based on the Builder pattern for your class.

When you build the Player class, you will get an equivalent class with the Lombok generated code, like this.

public class Player {
    private String name;
    private int score;

    public static Player.PlayerBuilder builder() {
        return new Player.PlayerBuilder();
    }

    public String getName() {
        return this.name;
    }

    public int getScore() {
        return this.score;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setScore(int score) {
        this.score = score;
    }

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof Player)) {
            return false;
        } else {
            Player other = (Player)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                Object this$name = this.getName();
                Object other$name = other.getName();
                if (this$name == null) {
                    if (other$name == null) {
                        return this.getScore() == other.getScore();
                    }
                } else if (this$name.equals(other$name)) {
                    return this.getScore() == other.getScore();
                }

                return false;
            }
        }
    }

    protected boolean canEqual(Object other) {
        return other instanceof Player;
    }

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $name = this.getName();
        int result = result * 59 + ($name == null ? 43 : $name.hashCode());
        result = result * 59 + this.getScore();
        return result;
    }

    public String toString() {
        return "Player(name=" + this.getName() + ", score=" + this.getScore() + ")";
    }

    public Player(String name, int score) {
        this.name = name;
        this.score = score;
    }

    public Player() {
    }

    public static class PlayerBuilder {
        private String name;
        private int score;

        PlayerBuilder() {
        }

        public Player.PlayerBuilder name(String name) {
            this.name = name;
            return this;
        }

        public Player.PlayerBuilder score(int score) {
            this.score = score;
            return this;
        }

        public Player build() {
            return new Player(this.name, this.score);
        }

        public String toString() {
            return "Player.PlayerBuilder(name=" + this.name + ", score=" + this.score + ")";
        }
    }
}

The Tests

Next let’s go with the TDD approach and write the tests first. We will begin with an abstract test class, AbstractTest.

AbstractTest.java
package guru.springframework.consumer.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import guru.springframework.consumer.controller.domain.Player;
import org.junit.Before;
import org.springframework.boot.test.json.JacksonTester;

public abstract class AbstractTest {

  public JacksonTester<Player> json;
  @Before
  public void setup() {
    ObjectMapper objectMappper = new ObjectMapper();
    JacksonTester.initFields(this, objectMappper);
  }
}

 

This code initializes a JacksonTester object with an ObjectMapper that we will next use to write JSON data to test our controller.

Next is our controller test class. I have used Spring Boot Test and JUnit. I have a post on Spring Boot Test which you should go through if you are new to it. Also, if you are new to JUnit, I suggest you go through my JUnit series of posts.

The controller test class, GameEngineControllerTest.java is this.

GameEngineControllerTest.java
package guru.springframework.consumer.controller;

import guru.springframework.consumer.controller.domain.Player;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@AutoConfigureJsonTesters
@AutoConfigureStubRunner(workOffline = true, ids = "guru.springframework:game-api-producer:+:stubs:8090")
@DirtiesContext
public class GameEngineControllerTest extends AbstractTest{
    @Autowired
    MockMvc mockMvc;
    @Autowired GameEngineController gameEngineController;

    public JacksonTester<Player> json;

    @Test
    public void should_allow_to_play_when_score_is_above_500() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.post("/play/football")
                .contentType(MediaType.APPLICATION_JSON)
                .content(json.write(Player
                        .builder()
                        .name("Tim")
                        .score(600)
                        .build())
                        .getJson()))
                .andExpect(status().isOk())
                .andExpect(content().json("{'result':'ELIGIBLE'}"));

    }

    @Test
    public void should_not_allow_to_play_when_score_is_below_500() throws Exception {

        mockMvc.perform(MockMvcRequestBuilders.post("/play/football")
                .contentType(MediaType.APPLICATION_JSON)
                .content(json.write(Player
                        .builder()
                        .name("Tim")
                        .score(300)
                        .build())
                        .getJson()))
                .andExpect(status().isOk())
        .andExpect(content().json("{'result':'NOT ELIGIBLE'}"));

    }
}

 

In this code:

  • Line 20: Specifies SpringRunner as the test runner.
  • Line 21 bootstraps the test with Spring Boot’s support. The annotation works by creating the ApplicationContext for your tests through SpringApplication. The webEnvironment.MOCK attribute configures the “web environments” to start tests with a MOCK servlet environment.
  • Line 22 auto-configures MockMvc to be injected for us. MockMvc in the test will be providing a mock environment instead of the full-blown MVC environment that we get on starting the server.
  • Line 23 enable auto-configuration of JSON tester that we wrote earlier in AbstractTest.
  • Line 24 is important. Here we used @AutoConfigureStubRunner to instruct the Spring Cloud Contract stub runner to run the producer stub online. If you recall, we installed the producer stub in Maven local. The ids attribute specifies the group-ID and artifact-ID of the stub and instructs to start the stub on port 8090.
  • Line 25 @DirtiesContext indicates that the ApplicationContext associated with the test is dirty and should be closed. Thereafter, subsequent tests will be supplied a new context.
  • Line 27 – 29 autowires in MockMvc and the GameEngineController under test.
  • Line 33 – 44 uses MockMVC to test the controller for a Player with a score greater than 500.
  • Line 48 – 60 is the second test case that uses MockMVC to test the controller for a Player with score lesser than 500.

When you run the tests, as expected they fail. This is the first step in TDD where you write your tests first and see them fail.

The Controller Implementation

The controller for the Game Engine consumer is a REST controller with a single endpoint. We will implement the controller to make a REST request to the Producer stub in compliance with the Spring Cloud Groovy DSL contract.

Although in a real project, you will have a service layer making the REST request, for the sake of demonstration, I am doing it from the controller itself.

The controller code is this.

GameEngineController.java
package guru.springframework.consumer.controller;

import guru.springframework.consumer.controller.domain.Player;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.net.URI;


@RestController
public class GameEngineController {
    private final RestTemplate restTemplate;
    public GameEngineController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
    @PostMapping(path = "/play/{game}", consumes = "application/json", produces = "application/json")
   public ResponseEntity<String> playGame(@RequestBody Player player, @PathVariable String game){
        ResponseEntity<String> response = this.restTemplate.exchange(
        RequestEntity
                .post(URI.create("http://localhost:8090/gamemanager?game="+game))
                .contentType(MediaType.APPLICATION_JSON)
                .body(player),String.class);
                 return new ResponseEntity<String>(response.getBody(), HttpStatus.OK);
   }

}

 

The controller code uses a Spring injected RestTemplate to make a POST request to the producer stub and return back the response string as a ResponseEntity.

If you run the controller tests now, the tests pass.
Test Output of Spring Cloud Contract

Contract Violation

Let’s consider a scenario where the consumer violates the producer contract. To simulate this, we will update the test cases to do two things:

  1. Send a Person initialized only with the score field.
  2. Introduce a typo in the request URL.

Here is the code of the updated test cases.

 @Test
public void should_allow_to_play_when_score_is_above_500() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.post("/play/football")
            .contentType(MediaType.APPLICATION_JSON)
            .content(json.write(Player
                    .builder()
                   // .name("Tim")
                    .score(600)
                    .build())
                    .getJson()))
            .andExpect(status().isOk())
            .andExpect(content().json("{'result':'ELIGIBLE'}"));

}

@Test
public void should_not_allow_to_play_when_score_is_below_500() throws Exception {

    mockMvc.perform(MockMvcRequestBuilders.post("/play/footballs")
            .contentType(MediaType.APPLICATION_JSON)
            .content(json.write(Player
                    .builder()
                    .name("Tim")
                    .score(300)
                    .build())
                    .getJson()))
            .andExpect(status().isOk())
    .andExpect(content().json("{'result':'NOT ELIGIBLE'}"));

}

In the first test case, we commented out the statement that sets the player name. In the second test case, we intentionally introduced a typo in the request URL as /play/footballs instead of /play/football.

When we run the tests fail, as shown in this figure.

Test Output Contract Violation

As we can see, with Spring Cloud Contract we are enforcing a certain set of rules that we expect consumers to follow. A consumer violating any rule fails the tests.

When you are working in cloud-native architecture with CI and CD pipeline configured, this test failure will break your CI build. As a result, consumer code violating the contract would never get deployed to production.

Producer Tests

Coming back the Producer side of the contract, Spring Cloud Contract will also generate unit tests.

The generated tests are build from a base class. This allows you a lot of flexibility in how your tests are set up. And a minimum, you will need to mock the controller under test.

Recall the following configuration of Maven:

            <plugin>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-contract-maven-plugin</artifactId>
                <version>${spring-cloud-contract.version}</version>
                <extensions>true</extensions>
                <configuration>
                    <baseClassForTests>guru.springframework.GameBaseClass</baseClassForTests>
                </configuration>
            </plugin>

Here, in the configuration we are defining the base class for the tests.

Producer Base Test

Here is our implementation of the base class. This is a very minimal implementation. A more complex example could involve mocking services with Mockito.

public class GameBaseClass {

    @Before
    public void setUp() throws Exception {
        RestAssuredMockMvc.standaloneSetup(new GameController());
    }
}

For the test base class, I also added a controller, which is just an empty class.

@Controller
public class GameController  {
}

Spring Cloud Contract Generated Tests

Spring Cloud Contract will generate tests for our controller from the contracts we have defined.

In the example we have been following, the following tests are generated.

public class GameTest extends GameBaseClass {

    @Test
    public void validate_game_contract_for_score_greater_than_500() throws Exception {
        // given:
        MockMvcRequestSpecification request = given()
                .header("Content-Type", "application/json")
                .body("{\"name\":\"Tim\",\"score\":600}");

        // when:
        ResponseOptions response = given().spec(request)
                .queryParam("game", "football")
                .post("/gamemanager");

        // then:
        assertThat(response.statusCode()).isEqualTo(200);
        assertThat(response.header("Content-Type")).matches("application/json.*");
        // and:
        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
        assertThatJson(parsedJson).field("['result']").isEqualTo("ELIGIBLE");
    }

    @Test
    public void validate_game_contract_for_score_lesser_than_500() throws Exception {
        // given:
        MockMvcRequestSpecification request = given()
                .header("Content-Type", "application/json")
                .body("{\"name\":\"Tim\",\"score\":300}");

        // when:
        ResponseOptions response = given().spec(request)
                .queryParam("game", "football")
                .post("/gamemanager");

        // then:
        assertThat(response.statusCode()).isEqualTo(200);
        assertThat(response.header("Content-Type")).matches("application/json.*");
        // and:
        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
        assertThatJson(parsedJson).field("['result']").isEqualTo("NOT ELIGIBLE");
    }

}

You can see in the above example, the generated test class extends from the base class we configured and implemented.

If your following TDD, you would now have failing tests, and you would go and complete the implementation of the Game controller.

If you execute tests (mvn clean install), you will see the following output, showing the failing tests.

 [ERROR] Tests run: 2, Failures: 2, Errors: 0, Skipped: 0, Time elapsed: 1.444 s <<< FAILURE! - in guru.springframework.GameTest
[ERROR] validate_game_contract_for_score_lesser_than_500(guru.springframework.GameTest)  Time elapsed: 1.423 s  <<< FAILURE!
org.junit.ComparisonFailure: expected:<[200]> but was:<[404]>
	at guru.springframework.GameTest.validate_game_contract_for_score_lesser_than_500(GameTest.java:49)

[ERROR] validate_game_contract_for_score_greater_than_500(guru.springframework.GameTest)  Time elapsed: 0.021 s  <<< FAILURE!
org.junit.ComparisonFailure: expected:<[200]> but was:<[404]>
	at guru.springframework.GameTest.validate_game_contract_for_score_greater_than_500(GameTest.java:29)

[INFO] 
[INFO] Results:
[INFO] 
[ERROR] Failures: 
[ERROR]   GameTest.validate_game_contract_for_score_greater_than_500:29 expected:<[200]> but was:<[404]>
[ERROR]   GameTest.validate_game_contract_for_score_lesser_than_500:49 expected:<[200]> but was:<[404]>
[INFO] 
[ERROR] Tests run: 3, Failures: 2, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 9.152 s
[INFO] Finished at: 2018-08-12T12:00:14-04:00
[INFO] Final Memory: 63M/527M
[INFO] ------------------------------------------------------------------------

Summary

In these examples, I’ve only shown a few highlights of using Spring Cloud Contract. The project is very robust and offers rich functionality.

If you are building or consuming APIs developed by others, using Consumer Driven Contracts is a tool you can use to improve the quality of your software. And to avoid unexpected breaking changes.

About jt

    You May Also Like

    2 comments on “Using Spring Cloud Contract for Consumer Driven Contracts

    1. November 16, 2018 at 4:23 pm

      Is it really consumer-driven-contract? Contract file is created inside producer code. How can consumer define the contract? In my opinion, for CDC this contract should be placed outside producer source code and should be imported there from sources which are shared between consumer and producer.

      Reply
    2. March 5, 2020 at 9:26 am

      Where can I download the above example code

      Reply

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    This site uses Akismet to reduce spam. Learn how your comment data is processed.