In this tutorial, we keep the contracts in a separate repository (a common arrangement).

Scenarios

In most of the tutorials, you will be asked to code the following scenarios:

beer 1
Figure 1. Positive beer selling via HTTP

   

beer 2
Figure 2. Negative beer selling via HTTP

   

msg 1
Figure 3. Positive age verification via messaging

   

msg 2
Figure 4. Negative age verification via messaging

   

Flow

flow
Figure 5. Consumer Driven Contract flow

Tutorial

Using Consumer Driven Contracts is like using TDD at the architecture level. Let’s start by writing a test on the consumer side.

Consumer flow 1

external consumer flow 1
Figure 6. Interact with cloned repo with contracts
Important
For the sake of this tutorial, our consumer application is called beer-api-consumer.

IDE setup

  • Open in your IDE the consumer project (either via Maven or Gradle)

  • We have two objectives for HTTP

    • when a client wants a beer and has e.g. name "marcin" and age 22 - the answer that we’ll respond with THERE YOU GO

    • when a client is an underage and wants a beer and has e.g. name "marcin" and age 17 - the answer that we’ll respond with GET LOST

  • and we have two objectives for messaging

    • when a verification message with eligible field equal to true was sent to the verifications channel then we increment the eligible counter

    • when a verification message with eligible field equal to false was sent to the verifications channel then we increment the notEligible counter

  • Let’s start with HTTP.

  • Open the BeerControllerTest test. Since CDC is like TDD we have 2 tests that describe our beer selling features. and we’re already providing some basic setup for you (in real TDD example you’d have to code all of that yourself)

  • Technically speaking for both cases we want to use MockMvc to send a request to the /beer endpoint with a JSON pojo containing name and age. From the controller we want to send a request to http://localhost:8090/ where the producer will be waiting for out requests. Let’s write the missing tests body. (Show solution)

  • The first step in TDD is red - let’s run the tests and ensure that they are failing (in the controller we return null instead of any meaningful value)

  • The problem is such that we don’t yet know what API we would like to have…​ This is where we touch the very essence of Consumer Driven Contracts. As consumers we want to drive the change of the API that’s why, as consumers, we will work on the repo with contracts .

Cloning the repo with contracts

In this tutorial, we will not clone the repo with contracts. We’ll open it in the IDE. The repository is called beer_contracts.

Adding dependencies in the repo with contracts

Since we want the IDE to help us with code completion, let’s add the necessary Spring Cloud Contract dependencies to the pom.xml under beer_contracts repo. You need to add spring-cloud-starter-contract-verifier as a dependency, as shown in the following example:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-verifier</artifactId>
</dependency>
Important
Notice that we add the dependency in the compile scope, because we want all of the contracts to be packaged as if they were production code. This task is performed only once. When you add subsequent contracts, you won’t be able to add them again.

Creating the proper folder structure

In order to use a repository with contracts, you need to set up the folders so that they adhere to the following convention: ** /slash/separated/group/id/of/producer/producer-artifactid/consumer-name/ In this tutorial, our producer has the following coordinates: com.example:beer-api-producer-external. Our consumer will be called beer-api-consumer.

Create the contract directory by running the following commands at the command line:

$ cd beer_contracts
$ mkdir -p src/main/resources/contracts/com/example/beer-api-producer-external/beer-api-consumer/messaging
$ mkdir -p src/main/resources/contracts/com/example/beer-api-producer-external/beer-api-consumer/rest

We have successfully created the proper folder structure. Time to add some contracts

Defining first HTTP contract

Let’s move to the src/main/resources/contracts/com/example/beer-api-producer-external/beer-api-consumer/ folder. (Show solution).

  • You can define the contracts using Groovy DSL. Let’s create our first HTTP contract.

    • Under the rest folder create a file shouldGrantABeerIfOldEnough.groovy

    • Call the Contract.make method to start defining the contract

      org.springframework.cloud.contract.spec.Contract.make {
      
      }
    • You can call description() method to provide some meaningful description. TIP: You can use the Groovy multiline String """ """ to have all special characters escaped. Every new line in the String will be converted into a new line character. Example

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
      }
    • Now call the request { } and response { } methods

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          request {
          }
          response {
          }
      }
    • Let’s assume that we’re interested in sending a POST method. Call method POST() or method "POST". TIP: In Groovy you don’t need to provide parentheses (in most cases). You can write either method POST() or method(POST()). In both cases it’s the same syntax

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          request {
              method POST()
          }
          response {
          }
      }
    • Now we need to provide some URL. Let it be /check. Let’s write url "/check"

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          request {
              method POST()
              url "/check"
          }
          response {
          }
      }
    • Now time to define some body. We’ll leverage some of the Groovy power over here so if you’re lost you can always check the Groovy JSON documentation. Let’s call the body() method with brackets.

    • In Groovy you can use the map notation in such a way [key: "value", secondKey: 2]. In the same way we can describe the body of a JSON. So in order to send a JSON looking like this { "age": 22, "name": "marcin" } we can create a map notation of [age:22, name:"marcin"]. The body method accepts a map and in Groovy if a method accepts a map then the [] brackets can be omited. So you can either write body([age:22, name:"marcin"]) or body(age:22, name:"marcin"). The latter one contains less boilerplate code so let’s write that one

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          request {
              method POST()
              url "/check"
              body(
                  age: 22, name: "marcin"
                  )
          }
          response {
          }
      }
    • Now time for the headers…​ Call the headers { } method

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          request {
              method POST()
              url "/check"
              body(
                  age: 22, name: "marcin"
                  )
              headers {
      
              }
          }
          response {
          }
      }
    • Inside that method let’s define that we want to use the Content-Type: "application/json header. Just call contentType(applicationJson()) methods

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          request {
              method POST()
              url "/check"
              body(
                  age: 22, name: "marcin"
                  )
              headers {
                  contentType(applicationJson())
              }
          }
          response {
          }
      }
    • Congratulations! You defined how you would like the contract for the request to look like! Time for the response

    • In the response block we would like to define that that the status of our response will be 200. Just call status 200

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          request {
              method POST()
              url "/check"
              body(
                  age: 22, name: "marcin"
                  )
              headers {
                  contentType(applicationJson())
              }
          }
          response {
              status 200
          }
      }
    • We’d like our response to have some body. As you could have assumed there’s a body method here too. We’ll now use another way of defining bodies (which is the less preferred option in Spring Cloud Contract but still can be useful) - using String

    • We’re assuming that we would like to send back a field called status that will return OK when the person can get the beer

    • Call the body(""" { "status" : "OK" } """). That way you’re defining how the response body will look like by providing the exact JSON value

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          request {
              method POST()
              url "/check"
              body(
                  age: 22, name: "marcin"
                  )
              headers {
                  contentType(applicationJson())
              }
          }
          response {
              status 200
              body("""
                  { "status" : "OK" }
              """)
          }
      }
    • Last thing to add are the response headers. We’re doing exactly the same thing as we have done previously for the request. headers { contentType(applicationJson()) }.

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          request {
              method POST()
              url "/check"
              body(
                  age: 22, name: "marcin"
                  )
              headers {
                  contentType(applicationJson())
              }
          }
          response {
              status 200
              body("""
                  { "status" : "OK" }
              """)
              headers {
                  contentType(applicationJson())
              }
          }
      }
    • Congratulations! You have created your first contract!

Defining the Second HTTP Contract

Now it’s time for you to create the second contract. Under src/main/resources/contracts/com/example/beer-api-producer-external/beer-api-consumer/rest, create a file called shouldRejectABeerIfTooYoung.groovy. If you get lost, examine the solution. To create the second contract:

  1. Set the age in the request to 17.

  2. Update the response body to return a status of NOT_OK.

  3. Update the description.

Defining the First Messaging Contract

We’ve done the case for HTTP. Let’s move to the src/main/resources/contracts/com/example/beer-api-producer-external/beer-api-consumer/messaging folder.

  • Time to define some contracts for messaging. Create a shouldSendAcceptedVerification.groovy file. If you’re lost just check out the solution

    • Call the org.springframework.cloud.contract.spec.Contract.make { } method to start defining the contract

      org.springframework.cloud.contract.spec.Contract.make {
      }
    • You can call description() method to provide some meaningful description. TIP: You can use the Groovy multiline String """ """ to have all special characters escaped. Every new line in the String will be converted into a new line character

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
      }
    • HTTP communication is synchronous - you send a request and you get a response. With messaging the situation is different - a consumer suddenly might get a message. In the consumer tests the consumer needs a mean to trigger that message. That hook is called a label in Spring Cloud Contract. Let’s call our label accepted_verification. To define it in the contract just call the label method like this label 'accepted_verification'

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          label "accepted_verification"
      }
    • Next we define the message that we would like to receive. So from the producer’s perspective that’s an outputMessage. You can call that message in the Groovy DSL outputMessage { }

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          label "accepted_verification"
          outputMessage {
      
          }
      }
    • Inside that method we need to define where and what we want to send. Let’s start with the first. You can call the sentTo method and provide the destination. According to the requirements we want to send the message to the verifications channel. Let’s define that in the contract by calling sentTo 'verifications'

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          label "accepted_verification"
          outputMessage {
              sentTo "verifications"
          }
      }
    • As for the body we just can call body(eligible: true). That way we’ll send a JSON body via messaging

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          label "accepted_verification"
          outputMessage {
              sentTo "verifications"
              body(eligible: true)
          }
      }
    • We can also set headers on the message. Let’s call headers { } method and inside the closure we can set an explicit header. In case of messaging with Spring Cloud Stream, a header that describes the content type of the payload is called contentType. So we need to set it like this header("contentType", applicationJsonUtf8()).

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          label "accepted_verification"
          outputMessage {
              sentTo "verifications"
              body(eligible: true)
              headers {
                  header("contentType", applicationJsonUtf8())
              }
          }
      }

Congratulations! You have created your first messaging contract!

Defining second messaging contract

Now it’s time for you to create the second contract. Create a file called shouldSendRejectedVerification.groovy. If you get lost, examine the solution. To define the second message contract;

  1. Set the eligible property in the response body to false.

  2. Update the label to rejected_verification.

  3. Update the description.

Setting up Spring Cloud Contract in the Producer Contracts Config File inside the Contracts Repo

For a repository with contracts, we need a way for the consumer to convert the contracts into stubs and install them locally. At the moment, the easiest way is to use of Maven or Gradle. In this tutorial, we use Maven. You can see how the pom.xml should look in beer_contracts/example/pom.xml and in the following example:

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example</groupId>
	<artifactId>beer-api-producer-external</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<name>Beer API producer Stubs</name>
	<description>POM used to install locally stubs for consumer side</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.4.0</version>
		<relativePath/>
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>1.8</java.version>
		<spring-cloud-contract.version>2.4.0</spring-cloud-contract.version>
		<spring-cloud-dependencies.version>2020.0.4-SNAPSHOT</spring-cloud-dependencies.version>
		<!-- We don't want to run tests -->
		<skipTests>true</skipTests>
		<!-- We don't want to add build folders to the generated jar -->
		<excludeBuildFolders>true</excludeBuildFolders>
	</properties>

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

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-surefire-plugin</artifactId>
				<version>2.19.1</version>
				<configuration>
					<skipTests>true</skipTests>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-contract-maven-plugin</artifactId>
				<version>${spring-cloud-contract.version}</version>
				<extensions>true</extensions>
				<configuration>
					<!-- By default it would search under src/test/resources/ -->
					<contractsDirectory>${project.basedir}</contractsDirectory>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>
  • This configuration file:

    • Sets the jar name to the beer-api-producer-external (that’s how the producer’s called).

    • Disables test generation (we want only to generate stubs).

    • Adds the Spring Cloud Contract plugin (as you can see, there is no configuration related to base classes).

    • Sets excludeBuildFolders, which is pretty descriptive. When generating stubs, target and build folders are generated, and we don’t want them in the output jar.

    • Sets contractsDirectory. By default, Spring Cloud Contract searches under the src/test/resources/contracts folder. In this case, we have the contracts under / (from the point of view of the pom.xml file).

Now, as consumers, we want to convert the contracts into stubs so that we can use the API. We use Maven to achieve that.

Important
You must have a standalone version of Maven installed!

+

$ mvn clean install

Now you have successfully installed the stubs locally.

You can check out the target/stubs/META-INF/com.example/beer-api-producer/0.0.1-SNAPSHOT folder. In there, you see the contracts folder, where all contracts were copied, and the mappings folder, where you can find all the generated stubs. By default, Spring Cloud Contract uses WireMock as an implementation of fake HTTP server. Under the beer/rest subfolder, you can see all the generated stubs. Notice that we use JSON Paths to check the contents of the request.

Writing the missing consumer HTTP implementation

  • Let’s go back to our, consumer’s code - let’s go back to the BeerControllerTest and BeerController. We know how we would like the API to look like so now we can write the missing implementation in the BeerController. Let’s assume that the producer application will be running at http://localhost:8090/. Now go ahead and try to write the missing implementation of the BeerController

  • In case of any issues you can check out the solution

Turning on Stub Runner in HTTP consumer tests

  • Let’s run the BeerControllerTest again. It will fail since we’re trying to send a request to a real instance that hasn’t been started

  • Now it’s time to turn on the magic! Let’s add the Spring Cloud Starter Contract Stub Runner test dependency.

    Maven
    <dependency>
    	<groupId>org.springframework.cloud</groupId>
    	<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    	<scope>test</scope>
    </dependency>
    Gradle
    testImplementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
  • We can annotate our test class with @AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL, ids = "com.example:beer-api-producer-external:+:stubs:8090"). What that will do is:

    • it will download the stub JARs from Maven local (stubsMode = StubRunnerProperties.StubsMode.LOCAL)

    • it will search for a JAR with coordinates com.example:beer-api-producer-external with latest version (+) and stubs classifier. Once it’s found the fake HTTP server stub will be started at port 8090

  • Rerun the test - it should automagically pass!

    • In the logs you will see information about downloading, unpacking and starting stubs (see the logs)

    • What happened is that we could interact with real API without writing a single line of production code

Playing with the HTTP contracts

  • TDD is about red, green and refactor. We went through the first two. Time to refactor the code. We come to the conclusion that the name field is unnecessary. In the BeerController.java file let’s create a new class called BeerRequest that will contain only age field and let’s try to send that to our stubbed producer. (Show solution)

  • Let’s run the tests again - what will happen is that the tests will fail. That’s because in the contract you have explicitly described that you want the name to be there. As you can see all the typos will be caught during the build time of your project.

    • The same will happen if you leave the name but change the age to some other value (e.g. 28). Our stubs at the moment are very strict. We’ll try to fix that in a second

  • To fix this you need to go back with your IDE to the producer and modify your HTTP contracts.

    • Just remove the name field from the request body.

    • Spring Cloud Contract allows you to provide dynamic values for parts of body, urls, headers etc. This is especially useful when working with dates, database ids, UUIDs etc.

    • Let’s open the shouldGrantABeerIfOldEnough.groovy and go to the request body age element

    • Instead of 22 write $(regex('[2-9][0-9]')). Now let’s analyze what this is.

      • In order to tell Spring Cloud Contract that there will be a dynamic value set you have to use either the $() or value() method. They are equivalent.

      • Next we use regex() method that converts your String into Pattern. In this case we assume a 2 digit number greater or equal to 20

    • Repeat the same process for the shouldRejectABeerIfTooYoung.groovy contract but change the regular expression to [0-1][0-9]

    • Run the building with test skipping and check the output of stubs. You’ll see that the generated mappings have changed from equality check in JSON Path to regular expression check

    • Go back to the consumer code and run the BeerControlerTest again. This time it should pass. You can also change the values of age to e.g. 45 for the positive case and 11 for the negative on.

Writing the missing consumer messaging implementation

  • We’ve gone through the HTTP scenario and now it’s time for the messaging part.

  • Let' start with a test as usual. Let’s check out the BeerVerificationListenerTest test class

    • there are 2 test methods with empty bodies

    • in both cases we need to trigger a message that will get sent to a destination at which our listener class is awaiting messages

    • we’re missing the triggering part - but we’ll add it in a second

  • On the consumer side let’s check out the BeerVerificationListener class.

    • We’re using the Spring Cloud Stream’s abstraction of a queue / topic which is called a channel.

    • There are 2 channels that come out od the box with SC-Stream. These are input and output. Those channels can be found in 2 interfaces - Sink and Source. Sink contains the input channel which is used for listening for messages and Source contains the output channel which is used to send messages. In the listener class you can see that we use the Sink one cause we’re waiting for a message to be received.

    • We have to configure the destination, so the actual name of a queue / topic on which we will be listening. To do that you have to set in the src/main/resources/application.yml the property spring.cloud.stream.bindings.input-in-0.destination: verifications. That means that the we’ll use the input channel (so the channel in the Sink interface) to listen to messages coming from a destination called verifications.

    • Now that we have configured Spring Cloud Stream let’s write the missing feature. If the eligible flag in the incoming message is true - increase the eligibleCounter value. Otherwise increment the other notEligibleCounter one. (Show solution)

  • Now that the implementation is written - let’s try to run our BeerVerificationListenerTest tests. Unfortunately they will fail cause no message has been received - we’ll still missing that part

Turning on Stub Runner in messaging consumer tests

  • Time to use Spring Cloud Contract!

    • We need to use Spring Cloud Contract Stub Runner so that it downloads the stubs. Just add the @AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL, ids = "com.example:beer-api-producer-external") to download the latest stubs of com.example:beer-api-producer-external, with classifier stubs and if the JAR contains any HTTP stubs then register them at a random port.

    • Now we need a solution to trigger the message. To do that we need to autowire a StubTrigger interface. Just add @Autowired StubTrigger stubTrigger field to your test

    • In the contract on the producer side we’ve described 2 labels. accepted_verification and rejected_verification. You can use the StubTrigger#trigger method to trigger a message with a given label. For example if you call stubTrigger.trigger("accepted_verification") you’ll trigger a message that got described with the accepted_verification label.

    • Now add the missing StubTrigger#tigger method in the test bodies. (Show solution)

    • Run the tests and they should pass!

    • You can change the destination name in src/main/resources/application.yml to foo and rerun the tests - you’ll see that they’ll start failing. That’s because you’re listening to messages at destination foo whereas the message is sent to verifications

    • You can also play around with the Verification payload class. If you change the field name from eligible to foo an rerun the tests - the tests will fail. If you change the type from boolean to Integer (and change the production code too) then the tests will fail due to serialization problems

Congratulations! As a consumer, you successfully used the API of the producer for both HTTP and messaging. Now you can file a pull request (PR) to the repository that stores all contracts.

Producer Flow 1

external producer flow 1
Figure 7. Producer takes over the PR filed to the repo with contracts, writes missing impl on the producer side

IDE setup

Open producer_with_external_contracts project (either via Maven or Gradle) in your IDE.

This tutorial assumes that you have taken over the PR of the beer_contracts. The following git commands show an example of how to do so for a PR that got submitted to via a branch called the_pr:

git fetch origin
git checkout -b the_pr origin/the_pr
git merge master

The core idea of Spring Cloud Contract is stub and contract validity. Right now, you have a set of contracts defined but haven’t tested it against the producer side. It’s time to change that!

Install the Contracts Locally

In order for the producer to use the contracts, we need to produce a JAR with all the contracts. To achieve that, use Maven’s assembly plugin. You can find the configuration under beer_contracts/src/assembly/contracts.xml. We still need to configure the beer_contracts/pom.xml by providing the assembly plugin configuration.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-assembly-plugin</artifactId>
  <executions>
      <execution>
          <id>contracts</id>
          <phase>prepare-package</phase>
          <goals>
              <goal>single</goal>
          </goals>
          <configuration>
              <attach>true</attach>
              <descriptor>${basedir}/src/assembly/contracts.xml</descriptor>
              <appendAssemblyId>false</appendAssemblyId>
          </configuration>
      </execution>
  </executions>
</plugin>

The preceding snippet creates a beer-contracts JAR (in the target folder) that contains all contracts and poms from src/main/resources/contracts.

Setting up the Spring Cloud Contract Dependencies on the Producer Side

First, let’s add the Spring Cloud Contract Verifier dependency to the project, as shown in the following snippet:

Maven
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-contract-verifier</artifactId>
	<scope>test</scope>
</dependency>
Gradle
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier")

Spring Cloud Contract can generate tests from your contracts to ensure that your implementation’s API is compatible with the defined contract. Let’s set up the project to start generating tests.

Spring Cloud Contract needs a base class that all of the generated tests will extend. Currently, we support three different ways of defining a base class (you can read more about this in the Spring Cloud Contract documentation for Gradle and Maven).

  • A single class for all tests.

  • Convention-based naming (which takes the last two package names and appends Base. For example, a contract src/test/resources/contracts/foo/bar/shouldDoSth.groovy creates a test class called BarTest that would extend the FooBarBase class.

  • Manual mapping (you can state that contracts matching certain regular expressions must have a base class with fully qualified name equal to the value you specify).

Maven
<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>${spring-cloud-contract.version}</version>
    <extensions>true</extensions>
    <configuration>
        <baseClassMappings>
            <baseClassMapping>
                <contractPackageRegex>.*messaging.*</contractPackageRegex>
                <baseClassFQN>com.example.BeerMessagingBase</baseClassFQN>
            </baseClassMapping>
            <baseClassMapping>
                <contractPackageRegex>.*rest.*</contractPackageRegex>
                <baseClassFQN>com.example.BeerRestBase</baseClassFQN>
            </baseClassMapping>
        </baseClassMappings>
        <!-- We want to use the JAR with contracts with the following coordinates -->
        <contractDependency>
            <groupId>com.example</groupId>
            <artifactId>beer-contracts</artifactId>
        </contractDependency>
        <!-- The JAR with contracts should be taken from Maven local -->
        <contractsMode>LOCAL</contractsMode>
        <!-- Base package for generated tests -->
        <basePackageForTests>com.example</basePackageForTests>
    </configuration>
    <!-- this was added for testing purposes only -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-contract-verifier</artifactId>
            <version>${spring-cloud-contract.version}</version>
        </dependency>
    </dependencies>
</plugin>
Gradle
contracts {
	testFramework = "JUNIT5"
	// We want to use the JAR with contracts with the following coordinates
	contractDependency {
		stringNotation = 'com.example:beer-contracts'
	}
	// The JAR with contracts should be taken from Maven local
	contractsMode = "LOCAL"
	// Base package for generated tests
	basePackageForTests = "com.example"
	baseClassMappings {
		baseClassMapping(".*messaging.*", "com.example.BeerMessagingBase")
		baseClassMapping(".*rest.*", "com.example.BeerRestBase")
	}
}

In both cases, we define that we want to download the JAR with contracts with given coordinates (com.example:beer-contracts).

We do not provide the contractsRepositoryUrl (the URL from which we expect the contracts to be downloaded), because we want to work offline. That’s why we set the LOCAL mode.

We decided that all the generated tests should be generated under the com.example package instead of the default one.

We have manually set mappings of packages in which contracts are stored to a fully qualified name of the base class. If there’s a contract that has a messaging package name in its path, it will be mapped to a com.example.BeerMessagingBase base class.

Updating Contracts from the Pull Request

  • We need to modify the messaging contracts cause they are missing one important piece from the producer’s perspective - the input part

    • In case of messaging there has to be some trigger that will result in producing an output message

    • Spring Cloud Contract accepts 3 situations

      • Input message produces an output message

      • A method execution produces an output message

      • Input message doesn’t produce any output message

    • In our situation we’ll have a method produce an output. It’s enough to pass the input {} method and then the triggeredBy method. The triggeredBy method requires a String with a method execution. So if in the base class we expect to have a method called triggerSomeMessage() that would trigger a message for tests, then we would write input { triggeredBy("triggerSomeMessage()") } to make this happen. Example:

      org.springframework.cloud.contract.spec.Contract.make {
          description("""
              some interesting description
          """)
          label "accepted_verification"
          input {
              triggeredBy("triggerSomeMessage()")
          }
          outputMessage {
              sentTo "verifications"
              body(eligible: true)
              headers {
                  header("contentType", applicationJsonUtf8())
              }
          }
      }
    • For this workshop for the shouldSendAcceptedVerification.groovy we want to trigger the clientIsOldEnough() method and for shouldSendRejectedVerification.groovy we want to trigger the clientIsTooYoung() method from the base class. (Show solution)

Important
Remember to rebuild the JAR with contracts in the beer_contracts repo. You need to produce and install the JAR in your Maven local.

Generating tests from contracts

  • Let’s generate the tests! Just call:

    Maven
    $ ./mvnw clean install
    Gradle
    $ ./gradlew clean build publishToMavenLocal
    • Suddenly some tests should start failing. Those tests are the autogenerated tests created by Spring Cloud Contract

    • The tests lay under /generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/beer in target for Maven or build for Gradle

    • There will be a test for each folder in which you store your contracts. The name of the test class will be the name of that folder

    • Each of the contracts will be a single test inside that test class

    • If you check out the generated tests you’ll notice that the dynamic parts of the request part of the contract got converted to a concrete value. Any dynamic bits on the response side would be converted into matchers.

  • Time to fix the broken tests. We need to do that by providing the missing implementation.

Fixing broken HTTP tests

  • Let’s start with HTTP

    • First let’s write the missing implementation in ProducerController. The logic to be written is extremely simple - if the personCheckingService.shouldGetBeer(…​) returns true then we should return new Response(BeerCheckStatus.OK). Otherwise new Response(BeerCheckStatus.NOT_OK). (Show solution)

  • Let’s fix the BeerRestBase class now

    • The idea of CDC is NOT TO TEST every single feature. Contract tests are there to see if the API is matched, NOT that the feature is working. That’s why we shouldn’t be accessing databases etc. That means that we will work with mock of the PersonCheckingService. (Show solution)

    • Let’s annotate the test class with @RunWith(MockitoJUnitRunner.class) to enable Mockito runner.

      @RunWith(MockitoJUnitRunner.class)
      public abstract class BeerRestBase {
      ...
      }
    • We’ll want to test the ProducerController so we can create a field @InjectMocks ProducerController producerController. Mockito will inject any mocks for us via the constructor.

          @Mock PersonCheckingService personCheckingService;
          @InjectMocks ProducerController producerController;
      
          @BeforeEach
          public void setup() {
              given(personCheckingService.shouldGetBeer(argThat(oldEnough()))).willReturn(true);
          }
    • It won’t compile cause we don’t have the oldEnough() method but don’t worry. So this line stubs the shouldGetBeer method in such a way that if the user is old enough then the method will return true. Let’s now add the oldEnoughMethod()

      	private TypeSafeMatcher<PersonToCheck> oldEnough() {
      		return new TypeSafeMatcher<PersonToCheck>() {
      			@Override protected boolean matchesSafely(PersonToCheck personToCheck) {
      				return personToCheck.age >= 20;
      			}
      			@Override public void describeTo(Description description) {
      			}
      		};
      	}
    • We’re using the TypeSafeMatcher from Hamcrest to create a matcher for PersonToCheck. In this case if the person to check is older or is 20 then the method shouldGetBeer method will return true.

    • Now we need to configure RestAssured that is used by Spring Cloud Contract to send requests. In our case we want to profit from MockMvc. In order to set the ProducerController with RestAssured it’s enough to call // https://github.com/spring-cloud/spring-cloud-contract/issues/1428 EncoderConfig encoderConfig = new EncoderConfig().appendDefaultContentCharsetToContentTypeIfUndefined(false); RestAssuredMockMvc.config = new RestAssuredMockMvcConfig().encoderConfig(encoderConfig); RestAssuredMockMvc.standaloneSetup(producerController);

      @RunWith(MockitoJUnitRunner.class)
      public abstract class BeerRestBase {
      
          @Mock PersonCheckingService personCheckingService;
          @InjectMocks ProducerController producerController;
      
          @BeforeEach
          public void setup() {
              given(personCheckingService.shouldGetBeer(argThat(oldEnough()))).willReturn(true);
      
      		// https://github.com/spring-cloud/spring-cloud-contract/issues/1428
      		EncoderConfig encoderConfig = new EncoderConfig().appendDefaultContentCharsetToContentTypeIfUndefined(false);
      		RestAssuredMockMvc.config = new RestAssuredMockMvcConfig().encoderConfig(encoderConfig);
      		RestAssuredMockMvc.standaloneSetup(producerController);
          }
      
          private TypeSafeMatcher<PersonToCheck> oldEnough() {
              return new TypeSafeMatcher<PersonToCheck>() {
                  @Override protected boolean matchesSafely(PersonToCheck personToCheck) {
                      return personToCheck.age >= 20;
                  }
                  @Override public void describeTo(Description description) {
                  }
              };
          }
      }
    • With mocks and RestAssured setup - we’re ready to run our HTTP based autogenerated tests

Fixing broken messaging tests

  • Now let’s go to the messaging part.

  • Let’s check out the src/main/resources/application.yml file whether it contains the proper destination set for spring.cloud.stream.bindings.output.destination. If not then let’s set it to verifications - this is the queue / topic we’d like to receive the message from

  • We’re trying to do TDD so let’s move to BeerMessagingBase test class. The first thing we need to do is to add the @AutoConfigureMessageVerifier annotation on the test class. That will configure the setup related to messaging and Spring Cloud Contract.

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = ProducerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
    @AutoConfigureMessageVerifier
    @ImportAutoConfiguration(TestChannelBinderConfiguration.class)
    public abstract class BeerMessagingBase {
    ...
    }
  • We need to prepare some setup for our tests. To do that we’ll need to clear any remaining messages that could break our tests. To do that we’ll use the Spring Cloud Contract MessageVerifier abstraction (it allows to send and receive messages from e.g. Spring Cloud Stream, Sprig Integration, Apache Camel.)

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = ProducerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
    @AutoConfigureMessageVerifier
    @ImportAutoConfiguration(TestChannelBinderConfiguration.class)
    public abstract class BeerMessagingBase {
        @Inject MessageVerifier messaging;
    	@Autowired PersonCheckingService personCheckingService;
    
    	@BeforeEach
    	public void setup() {
    		// let's clear any remaining messages
    		// output == destination or channel name
    		this.messaging.receive("output", 100, TimeUnit.MILLISECONDS);
    	}
    
    	public void clientIsOldEnough() {
        }
    
        public void clientIsTooYoung() {
        }
    }
  • In the clientIsOldEnough() and clientIsTooYoung() we need the logic to trigger a message. What triggers a message will be the implementation of the PersonCheckingService#shouldGetBeer.

  • For clientIsOldEnough() we can use a PersonToCheck of age 25 for example and clientIsTooYoung can have age 5. (Show solution)

  • We can run the test which will obviously fail because we have a missing implementation. Let’s move to AgeCheckingPersonCheckingService

Writing the missing producer messaging implementation

  • We need to check if the person’s age is greater or equal to 20 - if that’s the case then the we need to send the properly generated Verification object. In order to send a message you can use the following code source.output().send(MessageBuilder.withPayload(new Verification(true)).build()). In this case we’re sending a message to the output channel (that is bound to verifications destination). (Show solution)

  • Let’s run the tests again - they should all pass!

  • Now let’s ensure that we can successfully publish artifacts to Maven local

    Maven
    $ ./mvnw clean install
    Gradle
    $ ./gradlew clean build publishToMavenLocal

Producer Flow 2

external producer flow 2
Figure 8. Once done the producer merges the PR to the repo with contracts. The CI system will produce a JAR with all contracts
  • Now you would merge the PR from beer_contracts repo to master and your CI system would build a fat jar and stubs

Producer Flow 3

external producer flow 3
Figure 9. Once the PR got merged, the CI built the JAR of the contracts repo then the producer’s code can use that JAR in its own building process

After the pull request gets merged and the beer-contracts artifact gets published, you need to switch off your offline work in the Spring Cloud Contract (we do not do so for this tutorial). In a "real life" situation you would need to update it as shown in the following snippet:

Maven
<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>${spring-cloud-contract.version}</version>
    <extensions>true</extensions>
    <configuration>
        <baseClassMappings>
            <baseClassMapping>
                <contractPackageRegex>.*messaging.*</contractPackageRegex>
                <baseClassFQN>com.example.BeerMessagingBase</baseClassFQN>
            </baseClassMapping>
            <baseClassMapping>
                <contractPackageRegex>.*rest.*</contractPackageRegex>
                <baseClassFQN>com.example.BeerRestBase</baseClassFQN>
            </baseClassMapping>
        </baseClassMappings>
        <!-- We want to use the JAR with contracts with the following coordinates -->
        <contractDependency>
            <groupId>com.example</groupId>
            <artifactId>beer-contracts</artifactId>
        </contractDependency>
        <!-- The JAR with contracts will get downloaded from an external repo -->
        <contractsRepositoryUrl>https://foo.bar/baz</contractsRepositoryUrl>
        <!-- Base package for generated tests -->
        <basePackageForTests>com.example</basePackageForTests>
    </configuration>
</plugin>

+

Gradle
contracts {
	testFramework = "JUNIT5"
	// We want to use the JAR with contracts with the following coordinates
	contractDependency {
		stringNotation = 'com.example:beer-contracts'
	}
	// The JAR with contracts will get downloaded from an external repo
	contracts {
	testFramework = "JUNIT5"
		repositoryUrl = "https://foo.bar/baz"
	}
	// Base package for generated tests
	basePackageForTests = "com.example"
	baseClassMappings {
		baseClassMapping(".*messaging.*", "com.example.BeerMessagingBase")
		baseClassMapping(".*rest.*", "com.example.BeerRestBase")
	}
}

Congratulations! you’ve completed the producer side of this tutorial!

Consumer flow 2

consumer flow 2
Figure 10. Switch to work online
  • After merging the PR the producer’s stubs reside in some Artifactory / Nexus instance

  • As consumers we no longer want to retrieve the stubs from our local Maven repository - we’d like to download them from the remote location

  • To do that (we won’t do that for the tutorial but you would do it in your "production" code) it’s enough to pass the repositoryRoot parameter and set the stubsMode to StubRunnerProperties.StubsMode.REMOTE

    @RunWith(SpringRunner.class)
    @SpringBootTest(webEnvironment = WebEnvironment.MOCK)
    @AutoConfigureMockMvc
    @AutoConfigureJsonTesters
    @AutoConfigureStubRunner(
    repositoryRoot="http://www.foo.com/bar,
    ids = "com.example:beer-api-producer-external:+:stubs:8090",
    stubsMode = StubRunnerProperties.StubsMode.REMOTE
    )
    @DirtiesContext
    public class YourTestOnTheConsumerSide extends AbstractTest {
    }
    • Another option is to pass the property stubrunner.repositoryRoot either as a system / environment property, or via an application.yml and stubrunner.stubs-mode equal to remote

Generating documentation from contracts

Another feature of Spring Cloud Contract is an option to easily create the documentation of the whole API of the producer. You can create the following test that will generate a contracts.adoc file under target/generated-snippets/ with description of contracts and with the contract bodies as such.

Tip
This test is a poor man’s version of the documentation generation. You can customize it as you wish - the current version is just to show you an example.
package docs;

import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter;
import org.springframework.core.io.Resource;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collection;
import java.util.regex.Pattern;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

@SpringJUnitConfig
public class GenerateAdocsFromContractsTests {

	// TODO: Can be parametrized
	@Value("classpath:contracts") Resource contracts;
	private static String header = "= Application Contracts\n" + "\n"
			+ "In the following document you'll be able to see all the contracts that are present for this application.\n"
			+ "\n" + "== Contracts\n";

	@Test public void should_convert_contracts_into_adoc() throws IOException {
		final StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.append(header);
		final Path rootDir = this.contracts.getFile().toPath();

		Files.walkFileTree(rootDir, new FileVisitor<Path>() {
			private Pattern pattern = Pattern.compile("^.*groovy$");

			@Override
			public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes atts)
					throws IOException {
				return FileVisitResult.CONTINUE;
			}

			@Override
			public FileVisitResult visitFile(Path path, BasicFileAttributes mainAtts)
					throws IOException {
				boolean matches = this.pattern.matcher(path.toString()).matches();
				if (matches) {
					appendContract(stringBuilder, path);
				}
				return FileVisitResult.CONTINUE;
			}

			@Override
			public FileVisitResult postVisitDirectory(Path path, IOException exc)
					throws IOException {
				return FileVisitResult.CONTINUE;
			}

			@Override public FileVisitResult visitFileFailed(Path path, IOException exc)
					throws IOException {
				// If the root directory has failed it makes no sense to continue
				return path.equals(rootDir) ?
						FileVisitResult.TERMINATE :
						FileVisitResult.CONTINUE;
			}
		});

		//String outputAdoc = asciidoctor.convert(stringBuilder.toString(), new HashMap<String, Object>());
		String outputAdoc = stringBuilder.toString();
		// TODO: Can be parametrized
		File outputDir = new File("target/generated-snippets");
		outputDir.mkdirs();
		// TODO: Can be parametrized
		File outputFile = new File(outputDir, "contracts.adoc");
		if (outputFile.exists()) {
			outputFile.delete();
		}
		if (outputFile.createNewFile()) {
			Files.write(outputFile.toPath(), outputAdoc.getBytes());
		}
	}

	static StringBuilder appendContract(final StringBuilder stringBuilder, Path path)
			throws IOException {
		Collection<Contract> contracts = ContractVerifierDslConverter.convertAsCollection(path.getParent().toFile(), path.toFile());
		// TODO: Can be parametrized
		contracts.forEach(contract -> {
			stringBuilder.append("### ")
					.append(path.getFileName().toString())
					.append("\n\n")
					.append(contract.getDescription())
					.append("\n\n")
					.append("#### Contract structure")
					.append("\n\n")
					.append("[source,java,indent=0]")
					.append("\n")
					.append("----")
					.append("\n")
					.append(fileAsString(path))
					.append("\n")
					.append("----")
					.append("\n\n");
		});
		return stringBuilder;
	}

	static String fileAsString(Path path) {
		try {
			byte[] encoded = Files.readAllBytes(path);
			return new String(encoded, StandardCharsets.UTF_8);
		}
		catch (IOException e) {
			throw new RuntimeException(e);
		}
	}
}

Solutions

Written consumer tests

	@Test
	public void should_give_me_a_beer_when_im_old_enough() throws Exception {
		//remove::start[]
		this.mockMvc.perform(MockMvcRequestBuilders.post("/beer")
				.contentType(MediaType.APPLICATION_JSON)
				.content(this.json.write(new Person("marcin", 22)).getJson()))
				.andExpect(status().isOk())
				.andExpect(content().string("THERE YOU GO"));
		//remove::end[]
	}

	@Test
	public void should_reject_a_beer_when_im_too_young() throws Exception {
		//remove::start[]
		this.mockMvc.perform(MockMvcRequestBuilders.post("/beer")
				.contentType(MediaType.APPLICATION_JSON)
				.content(this.json.write(new Person("marcin", 17)).getJson()))
				.andExpect(status().isOk())
				.andExpect(content().string("GET LOST"));
		//remove::end[]
	}

Adding Spring Cloud Contract Dependency

Maven
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-contract-verifier</artifactId>
	<scope>test</scope>
</dependency>
Gradle
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier")

Proposal of simple contracts by consumer

HTTP communication

Old Enough
// rest/shouldGrantABeerIfOldEnough.groovy
org.springframework.cloud.contract.spec.Contract.make {
		description("""
Represents a successful scenario of getting a beer

```
given:
	client is old enough
when:
	he applies for a beer
then:
	we'll grant him the beer
```

""")
	request {
		method 'POST'
		url '/check'
		body(
				age: 22,
				name: "marcin"
		)
		headers {
			contentType(applicationJson())
		}
	}
	response {
		status 200
		body("""
			{
				"status": "OK"
			}
			""")
		headers {
			contentType(applicationJson())
		}
	}
}
Too Young
// rest/shouldRejectABeerIfTooYoung.groovy
org.springframework.cloud.contract.spec.Contract.make {
		description("""
Represents a successful scenario of getting a beer

```
given:
	client is old enough
when:
	he applies for a beer
then:
	we'll grant him the beer
```

""")
	request {
		method 'POST'
		url '/check'
		body(
				age: 17,
				name: "marcin"
		)
		headers {
			contentType(applicationJson())
		}
	}
	response {
		status 200
		body("""
			{
				"status": "NOT_OK"
			}
			""")
		headers {
			contentType(applicationJson())
		}
	}
}

Messaging communication

Positive Verification
// messaging/shouldSendAcceptedVerification.groovy
org.springframework.cloud.contract.spec.Contract.make {
	description("""
Sends a positive verification message when person is eligible to get the beer

```
given:
	client is old enough
when:
	he applies for a beer
then:
	we'll send a message with a positive verification
```

""")
	// Label by means of which the output message can be triggered
	label 'accepted_verification'
	// output message of the contract
	outputMessage {
		// destination to which the output message will be sent
		sentTo 'verifications'
		// the body of the output message
		body(
            eligible: true
		)
		headers {
			header("contentType", applicationJsonUtf8())
		}
	}
}
Negative Verification
// messaging/shouldSendRejectedVerification.groovy
org.springframework.cloud.contract.spec.Contract.make {
	description("""
Sends a negative verification message when person is not eligible to get the beer

```
given:
	client is too young
when:
	he applies for a beer
then:
	we'll send a message with a negative verification
```

""")
	// Label by means of which the output message can be triggered
	label 'rejected_verification'
	// output message of the contract
	outputMessage {
		// destination to which the output message will be sent
		sentTo 'verifications'
		// the body of the output message
		body(
            eligible: false
		)
		headers {
			header("contentType", applicationJsonUtf8())
		}
	}
}

Missing consumer controller code

		ResponseEntity<Response> response = this.restTemplate.exchange(
				RequestEntity
						.post(URI.create("http://localhost:" + this.port + "/check"))
						.contentType(MediaType.APPLICATION_JSON)
						.body(person),
				Response.class);
		switch (response.getBody().status) {
		case OK:
			return "THERE YOU GO";
		default:
			return "GET LOST";
		}

Stub Logs

2017-05-11 12:16:51.146  INFO 4693 --- [           main] o.s.c.c.s.StubDownloaderBuilderProvider  : Will download stubs using Aether
2017-05-11 12:16:51.148  INFO 4693 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Remote repos not passed but the switch to work offline was set. Stubs will be used from your local Maven repository.
2017-05-11 12:16:51.291  INFO 4693 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Desired version is [+] - will try to resolve the latest version
2017-05-11 12:16:51.308  INFO 4693 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved version is [0.0.1-SNAPSHOT]
2017-05-11 12:16:51.309  INFO 4693 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolving artifact [com.example:{producer_artifact}:jar:stubs:0.0.1-SNAPSHOT] using remote repositories []
2017-05-11 12:16:51.317  INFO 4693 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved artifact [com.example:{producer_artifact}:jar:stubs:0.0.1-SNAPSHOT] to /home/marcin/.m2/repository/com/example/{producer_artifact}/0.0.1-SNAPSHOT/{producer_artifact}-0.0.1-SNAPSHOT-stubs.jar
2017-05-11 12:16:51.322  INFO 4693 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacking stub from JAR [URI: file:/home/marcin/.m2/repository/com/example/{producer_artifact}/0.0.1-SNAPSHOT/{producer_artifact}-0.0.1-SNAPSHOT-stubs.jar]
2017-05-11 12:16:51.327  INFO 4693 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacked file to [/tmp/contracts9053257535983128167]
2017-05-11 12:16:52.608  INFO 4693 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@699e0bf0: startup date [Thu May 11 12:16:52 CEST 2017]; root of context hierarchy
2017-05-11 12:16:52.684  INFO 4693 --- [           main] f.a.AutowiredAnnotationBeanPostProcessor : JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2017-05-11 12:16:52.837  INFO 4693 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8090 (http)
2017-05-11 12:16:52.851  INFO 4693 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2017-05-11 12:16:52.853  INFO 4693 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.14
2017-05-11 12:16:52.975  INFO 4693 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2017-05-11 12:16:52.975  INFO 4693 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 367 ms
2017-05-11 12:16:52.996  INFO 4693 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'stub' to [/]
2017-05-11 12:16:53.000  INFO 4693 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'admin' to [/__admin/*]
2017-05-11 12:16:53.135  INFO 4693 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8090 (http)
2017-05-11 12:16:53.139  INFO 4693 --- [           main] o.s.c.contract.stubrunner.StubServer     : Started stub server for project [com.example:{producer_artifact}:0.0.1-SNAPSHOT:stubs] on port 8090

Beer Request

class BeerRequest {
	public int age;

	public BeerRequest(int age) {
		this.age = age;
	}

	public BeerRequest() {
	}
}

Missing listener code

		if (verification.eligible) {
			this.eligibleCounter.incrementAndGet();
		} else {
			this.notEligibleCounter.incrementAndGet();
		}

Missing triggers

	@Test public void should_increase_the_eligible_counter_when_verification_was_accepted() throws Exception {
		int initialCounter = this.listener.eligibleCounter.get();

		//remove::start[]
		this.stubTrigger.trigger("accepted_verification");
		//remove::end[]

		then(this.listener.eligibleCounter.get()).isGreaterThan(initialCounter);
	}

	@Test public void should_increase_the_noteligible_counter_when_verification_was_rejected() throws Exception {
		int initialCounter = this.listener.notEligibleCounter.get();

		//remove::start[]
		this.stubTrigger.trigger("rejected_verification");
		//remove::end[]

		then(this.listener.notEligibleCounter.get()).isGreaterThan(initialCounter);
	}

Messaging DSLs

Positive Verification
// messaging/shouldSendAcceptedVerification.groovy
org.springframework.cloud.contract.spec.Contract.make {
	description("""
Sends a positive verification message when person is eligible to get the beer

```
given:
	client is old enough
when:
	he applies for a beer
then:
	we'll send a message with a positive verification
```

""")
	// Label by means of which the output message can be triggered
	label 'accepted_verification'
	// input to the contract
	input {
		// the contract will be triggered by a method
		triggeredBy('clientIsOldEnough()')
	}
	// output message of the contract
	outputMessage {
		// destination to which the output message will be sent
		sentTo 'verifications'
		// the body of the output message
		body(
            eligible: true
		)
		headers {
			header("contentType", applicationJsonUtf8())
		}
	}
}
Negative Verification
// messaging/shouldSendRejectedVerification.groovy
org.springframework.cloud.contract.spec.Contract.make {
	description("""
Sends a negative verification message when person is not eligible to get the beer

```
given:
	client is too young
when:
	he applies for a beer
then:
	we'll send a message with a negative verification
```

""")
	// Label by means of which the output message can be triggered
	label 'rejected_verification'
	// input to the contract
	input {
		// the contract will be triggered by a method
		triggeredBy('clientIsTooYoung()')
	}
	// output message of the contract
	outputMessage {
		// destination to which the output message will be sent
		sentTo 'verifications'
		// the body of the output message
		body(
            eligible: false
		)
		headers {
			header("contentType", applicationJsonUtf8())
		}
	}
}

ProducerController implementation

if (personCheckingService.shouldGetBeer(personToCheck)) {
    return new Response(BeerCheckStatus.OK);
}
return new Response(BeerCheckStatus.NOT_OK);

BeerRestBase

@RunWith(MockitoJUnitRunner.class)
public abstract class BeerRestBase {
	@Mock PersonCheckingService personCheckingService;
	@InjectMocks ProducerController producerController;

	@BeforeEach
	public void setup() {
		given(personCheckingService.shouldGetBeer(argThat(oldEnough()))).willReturn(true);

		// https://github.com/spring-cloud/spring-cloud-contract/issues/1428
		EncoderConfig encoderConfig = new EncoderConfig().appendDefaultContentCharsetToContentTypeIfUndefined(false);
		RestAssuredMockMvc.config = new RestAssuredMockMvcConfig().encoderConfig(encoderConfig);
		RestAssuredMockMvc.standaloneSetup(producerController);
	}

	private TypeSafeMatcher<PersonToCheck> oldEnough() {
		return new TypeSafeMatcher<PersonToCheck>() {
			@Override protected boolean matchesSafely(PersonToCheck personToCheck) {
				return personToCheck.age >= 20;
			}

			@Override public void describeTo(Description description) {

			}
		};
	}
}

BeerMessagingBase

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ProducerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
@AutoConfigureMessageVerifier
@ImportAutoConfiguration(TestChannelBinderConfiguration.class)
public abstract class BeerMessagingBase {
	@Inject MessageVerifier messaging;
	@Autowired PersonCheckingService personCheckingService;

	@BeforeEach
	public void setup() {
		// let's clear any remaining messages
		// output == destination or channel name
		this.messaging.receive("output", 100, TimeUnit.MILLISECONDS);
	}

	public void clientIsOldEnough() {
		personCheckingService.shouldGetBeer(new PersonToCheck(25));
	}

	public void clientIsTooYoung() {
		personCheckingService.shouldGetBeer(new PersonToCheck(5));
	}
}

Messaging implementation

		boolean shouldGetBeer = personToCheck.age >= 20;
		this.source.send("output-out-0", new Verification(shouldGetBeer));
		return shouldGetBeer;

Back to the Main Page