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:




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

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 totrue
was sent to theverifications
channel then we increment theeligible
counter -
when a verification message with
eligible
field equal tofalse
was sent to theverifications
channel then we increment thenotEligible
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 containingname
andage
. From the controller we want to send a request tohttp://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 returnnull
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 fileshouldGrantABeerIfOldEnough.groovy
-
Call the
Contract.make
method to start defining the contractorg.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. Exampleorg.springframework.cloud.contract.spec.Contract.make { description(""" some interesting description """) }
-
Now call the
request { }
andresponse { }
methodsorg.springframework.cloud.contract.spec.Contract.make { description(""" some interesting description """) request { } response { } }
-
Let’s assume that we’re interested in sending a
POST
method. Callmethod POST()
ormethod "POST"
. TIP: In Groovy you don’t need to provide parentheses (in most cases). You can write eithermethod POST()
ormethod(POST())
. In both cases it’s the same syntaxorg.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 writeurl "/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"]
. Thebody
method accepts a map and in Groovy if a method accepts a map then the[]
brackets can be omited. So you can either writebody([age:22, name:"marcin"])
orbody(age:22, name:"marcin")
. The latter one contains less boilerplate code so let’s write that oneorg.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 { }
methodorg.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 callcontentType(applicationJson())
methodsorg.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 callstatus 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 returnOK
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 valueorg.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:
-
Set the
age
in the request to17
. -
Update the response body to return a
status
ofNOT_OK
. -
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 contractorg.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 characterorg.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 labelaccepted_verification
. To define it in the contract just call thelabel
method like thislabel '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 DSLoutputMessage { }
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 theverifications
channel. Let’s define that in the contract by callingsentTo '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 messagingorg.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 calledcontentType
. So we need to set it like thisheader("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;
-
Set the
eligible
property in the response body tofalse
. -
Update the label to
rejected_verification
. -
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
andbuild
folders are generated, and we don’t want them in the output jar. -
Sets
contractsDirectory
. By default, Spring Cloud Contract searches under thesrc/test/resources/contracts
folder. In this case, we have the contracts under/
(from the point of view of thepom.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
andBeerController
. We know how we would like the API to look like so now we can write the missing implementation in theBeerController
. Let’s assume that the producer application will be running athttp://localhost:8090/
. Now go ahead and try to write the missing implementation of theBeerController
-
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>
GradletestImplementation("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 (+
) andstubs
classifier. Once it’s found the fake HTTP server stub will be started at port8090
-
-
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 theBeerController.java
file let’s create a new class calledBeerRequest
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 theage
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 bodyage
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
$()
orvalue()
method. They are equivalent. -
Next we use
regex()
method that converts yourString
intoPattern
. In this case we assume a 2 digit number greater or equal to20
-
-
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 and11
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
andoutput
. Those channels can be found in 2 interfaces -Sink
andSource
.Sink
contains theinput
channel which is used for listening for messages andSource
contains theoutput
channel which is used to send messages. In the listener class you can see that we use theSink
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 thesrc/main/resources/application.yml
the propertyspring.cloud.stream.bindings.input-in-0.destination: verifications
. That means that the we’ll use theinput
channel (so the channel in theSink
interface) to listen to messages coming from a destination calledverifications
. -
Now that we have configured Spring Cloud Stream let’s write the missing feature. If the
eligible
flag in the incoming message istrue
- increase theeligibleCounter
value. Otherwise increment the othernotEligibleCounter
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 ofcom.example:beer-api-producer-external
, with classifierstubs
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
andrejected_verification
. You can use theStubTrigger#trigger
method to trigger a message with a given label. For example if you callstubTrigger.trigger("accepted_verification")
you’ll trigger a message that got described with theaccepted_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 insrc/main/resources/application.yml
tofoo
and rerun the tests - you’ll see that they’ll start failing. That’s because you’re listening to messages at destinationfoo
whereas the message is sent toverifications
-
You can also play around with the
Verification
payload class. If you change the field name fromeligible
tofoo
an rerun the tests - the tests will fail. If you change the type fromboolean
toInteger
(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

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:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
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 contractsrc/test/resources/contracts/foo/bar/shouldDoSth.groovy
creates a test class calledBarTest
that would extend theFooBarBase
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).
<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>
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 thetriggeredBy
method. ThetriggeredBy
method requires a String with a method execution. So if in the base class we expect to have a method calledtriggerSomeMessage()
that would trigger a message for tests, then we would writeinput { 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 theclientIsOldEnough()
method and forshouldSendRejectedVerification.groovy
we want to trigger theclientIsTooYoung()
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
intarget
for Maven orbuild
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 theresponse
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 thepersonCheckingService.shouldGetBeer(…)
returnstrue
then we should returnnew Response(BeerCheckStatus.OK)
. Otherwisenew 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 theshouldGetBeer
method in such a way that if the user is old enough then the method will return true. Let’s now add theoldEnoughMethod()
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 forPersonToCheck
. In this case if the person to check is older or is 20 then the methodshouldGetBeer
method will returntrue
. -
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 forspring.cloud.stream.bindings.output.destination
. If not then let’s set it toverifications
- 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()
andclientIsTooYoung()
we need the logic to trigger a message. What triggers a message will be the implementation of thePersonCheckingService#shouldGetBeer
. -
For
clientIsOldEnough()
we can use aPersonToCheck
of age25
for example andclientIsTooYoung
can have age5
. (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 codesource.output().send(MessageBuilder.withPayload(new Verification(true)).build())
. In this case we’re sending a message to theoutput
channel (that is bound toverifications
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

-
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

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:
<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>
+
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

-
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 thestubsMode
toStubRunnerProperties.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 anapplication.yml
andstubrunner.stubs-mode
equal toremote
-
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
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier")
Proposal of simple contracts by consumer
HTTP communication
// 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())
}
}
}
// 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
// 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())
}
}
}
// 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
// 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())
}
}
}
// 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;