In this tutorial, we’ll combine messaging contracts from the producer side with generating stubs from Spring Rest Docs.
Scenarios
In most of the tutorials, you will be asked to code the following scenarios:




Flow

Tutorial
This time, the producer defines the contracts and generates stubs. This is typically the case when your application has many consumers and it would be very difficult to take every consumer’s opinion into account.
Producer flow 1

IDE Setup for the Producer Scenario
To set up your IDE for this tutorial:
-
In your IDE, open the
producer_with_restdocs
project (via either Maven or Gradle). -
Add the necessary dependencies, as shown in the next section.
Adding Dependencies to the Producer’s Code
We’ll use Rest Docs with Spring Cloud Contract to generate HTTP stubs, and we’ll write the DSL contracts for messaging. In order to add Rest Docs, add the following test dependencies:
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<optional>true</optional>
</dependency>
testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc")
In order to use Spring Cloud Contract Rest Docs integration, you have to add the
spring-cloud-contract-wiremock
dependency. That way, we can generate the
WireMock stubs from our Rest Docs tests.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<scope>test</scope>
</dependency>
testImplementation("org.springframework.cloud:spring-cloud-contract-wiremock")
To get the IDE to help us with code completion in writing DSL contracts, we can add the
necessary Spring Cloud Contract dependencies. You need to add
spring-cloud-starter-contract-verifier
as a test 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")
You need to this task only once, because, when you add contracts, all the dependencies are already added.
Setting up the Spring Cloud Contract Plugin & Assembly Plugin
We need to use both the Spring Cloud Contract plugin & the assembly plugin. We use the Spring Cloud Contract plugin to generate tests for messaging contracts. We use the Assembly plugin to generate the JAR that holds the messaging contracts and HTTP stubs.
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 messaging tests.
By default, Spring Cloud Contract plugin creates the JAR with stubs. We need to disable that behavior.
Spring Cloud Contract needs a base class that all of the generated tests 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).
In the following example, we use convention-based naming. For Maven, under the plugin
setup, you must set up the plugin configuration as follows:
<configuration><packageWithBaseClasses>com.example</packageWithBaseClasses></configuration>
<properties>
<!-- we don't want the spring cloud contract plugin to do a jar for us -->
<spring.cloud.contract.verifier.skip>true</spring.cloud.contract.verifier.skip>
</properties>
<plugins>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<packageWithBaseClasses>com.example</packageWithBaseClasses>
</configuration>
</plugin>
<!-- we want the assembly plugin to generate the JAR -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>stub</id>
<phase>prepare-package</phase>
<goals>
<goal>single</goal>
</goals>
<inherited>false</inherited>
<configuration>
<attach>true</attach>
<descriptor>${basedir}/src/assembly/stub.xml</descriptor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
contracts {
testFramework = "JUNIT5"
packageWithBaseClasses = 'com.example'
}
task stubsJar(type: Jar) {
classifier = "stubs"
into('/') {
include('**/com/example/model/*.*')
from("${project.rootDir}/src/main/java/")
}
into('/') {
include('**/com/example/model/*.*')
from("${project.buildDir}/classes/main")
}
into("META-INF/${project.group}/${project.name}/${project.version}/mappings") {
include('**/*.*')
from("${project.rootDir}/target/snippets/stubs")
}
into("META-INF/${project.group}/${project.name}/${project.version}/contracts") {
include('**/*.groovy')
from("${project.rootDir}/src/test/resources/contracts")
}
}
// we need the tests to pass to build the stub jar
stubsJar.dependsOn(test)
artifacts {
archives stubsJar
}
In both cases, passing that value tells the plugin that a given base class is available
under the com.example
package. Also, it creates a stub jar in a custom way. For Maven,
it uses the assembly plugin with the configuration defined under src/assembly/stub.xml
.
For Gradle, it uses the assembly plugin through the stubsJar
task. The stubs jar
contains:
* The classes and sources of the POJO models.
* The contracts, under
META-INF/${project.group}/${project.name}/${project.version}/contracts
.
* The stubs, under
META-INF/${project.group}/${project.name}/${project.version}/mappings
.
An example for com.example:beer-producer:0.0.1-SNAPSHOT
would be
META-INF/com.example/beer-producer/0.0.1-SNAPSHOT/contracts
.
Writing Your First Rest Docs Test
Open the ProducerController
class. You can see that we already prepared some basic
setup for you. Our controller accepts a JSON request with a PersonToCheck
body and
returns a JSON response of Response
type. The logic that checks whether a person is
eligible to get beer is done via the PersonCheckingService
interface.
We want to do TDD on the producer side, so let’s start with a test. To do so, open the
ProducerControllerTests
class. We need to add the Rest Docs support by annotating the
class in the following way:
@AutoConfigureRestDocs(outputDir = "target/snippets")
That way, any snippets produced by Rest Docs end up in the target/snippets
folder. We
need to write two tests - one for the client who is old enough and one for a client
who is too young.
As you can see, we set up the Spring context. Doing so requires a fake
implementation of the PersonCheckingService
, since we don’t want to access any
databases, send messages, and so on. To do that in the Config
class, at the bottom of
the test, you can register a bean of PersonCheckingService
type that returns true
when the age of the PersonToCheck
is greater than or equal to 20
.
(Show solution)
We use MockMvc to send a JSON request to the /check
endpoint. The body of the request
is the prepared PersonToCheck
object (hint: you can use the prepared JacksonTester
object to send that json
. In the response, for the positive scenario, we expect the
response to set the status
field equal to OK
(hint: .andExpect(jsonPath("$.status").value("OK")))
and set the status
field for for the negative scenario equal to NOT_OK
(hint:
.andExpect(jsonPath("$.status").value("NOT_OK")))
.
(Show solution)
Let’s run the tests. They fail because we have yet to write any implementation on the producer side. Let’s fix that.
In the ProducerController
class, write the missing implementation. If the
PersonCheckingService
returns true
when the PersonToCheck
is eligible to get beer,
then return the Response
with BeerCheckStatus
equal to OK
. Otherwise, the
BeerCheckStatus
should equal NOT_OK
.
(Show solution)
Let’s rerun the tests. Now they should pass. We have yet to create any stubs. It’s time to fix that.
Spring Cloud Contract WireMock comes with a handy method called
WireMockRestDocs.verify()
that you can pass to the Rest Doc’s andDo()
method. The
WireMockRestDocs.verify()
method lets you:
-
Register the request and the response to store it as stub.
-
Assert JSON path’s of the request via
jsonPath
method (that’s how you can check the dynamic bits of your response). -
Check the content type of the request via the
contentType()
method. -
Save the stored request and response information as a WireMock stub via
stub()
method. -
Access WireMock’s API to perform further request verification via the
wiremock()
method.
Spring Cloud Contract WireMock also comes with a
SpringCloudContractRestDocs.dslContract()
method that lets you generate a DSL contract
from your Rest Docs tests. This can be handy when you have a lot of Rest Docs tests and
would like to migrate to DSL tests. If you call the andDo()
method and pass to it the
MockMvcRestDocumentation.document(…,…)
, you’ll create a dsl-contract.adoc
file
under the target/snippets/shouldRejectABeerIfTooYoung
folder and
shouldRejectABeerIfTooYoung.groovy
file under the target/snippets/contracts/
folder.
The code to do so follows:
.andDo(MockMvcRestDocumentation
.document("shouldRejectABeerIfTooYoung", SpringCloudContractRestDocs.dslContract()));
Important
|
To make this work, you must first call the WireMockRestDocs.verify() method
and only after that call the SpringCloudContractRestDocs.dslContract() method.
|
Now you can add the Spring Cloud Contract Rest Docs support, as (shown here). To do so:
-
For the positive scenario, assert that the
age
is greater or equal to20
(hint: the JSON path for this check is$[?(@.age >= 20)]
). -
For the negative scenario, assert that the
age
is less than20
(hint: the JSON path for this check is$[?(@.age < 20)]
) -
Assert that the request header contains a
content-type
ofapplicaton/json
(hint: you can use theMediaType
method:MediaType.valueOf("application/json")
). -
Produce the stub and
shouldGrantABeerIfOldEnough
the its documentation called for the positive scenario. -
Produce the stub called
shouldRejectABeerIfTooYoung
and its DSL documentation for the negative scenario.
Congratulations! In your target/snippets
you should see:
-
contracts/shouldGrantABeerIfOldEnough.groovy
, which is a file with the DSL contract for the positive scenario. -
contracts/target/snippets/.groovy
, which is a file with the DSL contract for the negative scenario. // TODO Missing file name? -
shouldGrantABeerIfOldEnough/
, which is a folder withadoc
files containing documentation of the positive scenario. -
shouldRejectABeerIfTooYoung/
, which is a folder withadoc
files containing documentation of the negative scenario. -
stubs/shouldGrantABeerIfOldEnough.json
, which is a WireMock stub of the positive scenario. -
stubs/shouldRejectABeerIfTooYoung.json
, which is a WireMock stub of the negative scenario.
Now we xan define the messaging contracts.
Defining the First Messaging Contract
We’ve done the case for HTTP. Now we can move to the
src/test/resources/contracts/beer/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()) } } }
-
-
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)
-
Defining the Second Messaging Contract
Now you can create the second contract. Create a file called shouldSendRejectedVerification.groovy
.
If you get lost, check out the solution. To
create the contract:
-
Set the
eligible
property in the response body tofalse
. -
Update the label to
rejected_verification
. -
Update the description.
Generating Tests from Contracts
Now we can generate the tests. To do so, call:
+
$ ./mvnw clean install
$ ./gradlew clean build publishToMavenLocal
Suddenly some tests should start failing. Those tests are the autogenerated tests created
by Spring Cloud Contract. The tests are under the
/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/beer
directory in the target
directory for Maven or the build
directory for Gradle.
There is a test for each folder in which you store your contracts. The name of the test
class is the name of that folder.
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
Checking the Generated JAR File
Let’s check out what’s inside the generated stub JAR. Assuming that our configuration is OK, if you run the following command, you should see output that resembles the following:
$ unzip -l target/beer-api-producer-restdocs-0.0.1-SNAPSHOT-stubs.jar
Archive: beer-api-producer-restdocs-0.0.1-SNAPSHOT-stubs.jar
Length Date Time Name
--------- ---------- ----- ----
164 2016-11-10 08:34 com/example/model/PersonToCheck.java
0 2017-05-12 13:40 META-INF/com.example/beer-api-producer-restdocs/
0 2017-05-12 13:40 META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/mappings/
569 2017-05-12 13:40 META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/mappings/shouldRejectABeerIfTooYoung.json
157 2016-11-10 08:34 com/example/model/Response.java
423 2017-05-12 13:40 com/example/model/PersonToCheck.class
0 2017-05-12 13:40 META-INF/
414 2017-05-12 13:40 com/example/model/Response.class
0 2017-05-12 13:40 com/example/model/
1015 2017-05-12 13:40 com/example/model/BeerCheckStatus.class
71 2016-11-10 08:34 com/example/model/BeerCheckStatus.java
0 2017-05-12 13:40 META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/contracts/
0 2017-05-12 13:40 META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/contracts/beer/
0 2017-05-12 13:40 META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/contracts/beer/messaging/
742 2017-05-12 13:38 META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/contracts/beer/messaging/shouldSendAcceptedVerification.groovy
0 2017-05-12 13:40 com/
595 2017-05-12 13:40 com/example/model/Verification.class
105 2017-05-12 13:40 META-INF/MANIFEST.MF
309 2016-11-10 08:34 com/example/model/Verification.java
0 2017-05-12 13:40 com/example/
0 2017-05-12 13:40 META-INF/com.example/
0 2017-05-12 13:40 META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/
566 2017-05-12 13:40 META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/mappings/shouldGrantABeerIfOldEnough.json
745 2017-05-12 13:38 META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/contracts/beer/messaging/shouldSendRejectedVerification.groovy
--------- -------
5875 24 files
-
Under
com/example/model
, you can see the compiled POJOs with sources. -
Under
META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/contracts
, you can see the messaging contracts. -
Under
META-INF/com.example/beer-api-producer-restdocs/0.0.1-SNAPSHOT/mappings
, you can see all the generated HTTP stubs.
In a "real life" scenario, we would merge our code into a fat jar, and a jar with stubs would be generated by the CI system. In this tutorial, we work with our Maven local, so that we don’t have to do anything else.
Now we can move to the consumer side.
Consumer Flow 2

In this part of the tutorial, we show different ways of working with stubs:
-
Using the
@AutoConfigureWireMock
annotation to manually pass a list of stubs to register from the classpath. -
Using the
@AutoConfigureStubRunner
annotation with classpath scanning. -
Using the
@AutoConfigureStubRunner
annotation for offline work
Important
|
This feature is available as of the Spring Cloud Contract 1.1.1.RELEASE. |
Adding Spring Cloud Contract
To add Spring Cloud Contract, we’ll do each of the following:
Reading HTTP Stubs from the Classpath with Spring Cloud Contract WireMock
In your IDE, open the consumer code from the consumer_with_restdocs
directory. We want
to do TDD, so let’s open the BeerControllerTest
class.
-
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)
Since the producer has already published its stubs, we already know how the API looks.
Let’s write the missing implementation for the BeerController
.
(Show solution)
If we run our tests again, they fail due to Connection Refused
. That’s because we try
to send a request to a non-started server.
Now you can turn on the magic! To do so, add the Spring Cloud Starter Contract Stub Runner test dependency.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
-
Now we’ll add the producer stub dependency to our project
<dependency>
<groupId>com.example</groupId>
<artifactId>beer-api-producer-restdocs</artifactId>
<classifier>stubs</classifier>
<version>0.0.1-SNAPSHOT</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
testImplementation("com.example:beer-api-producer-restdocs:0.0.1-SNAPSHOT:stubs") {
transitive = false
}
Important
|
Remember not to include any transitive dependencies. We want to import only the JAR that contains the contracts and stubs. |
Now we can annotate the BeerControllerTest
class with
@AutoConfigureWireMock(stubs = "classpath:/META-INF/com.example/beer-api-producer-restdocs/*/.json", port = 8090)
That annotation tells WireMock to start a fake HTTP server at port 8090
and to register
all stubs at the following location:
/META-INF/com.example/beer-api-producer-restdocs/*/.json
on the classpath
Let’s run our tests again. Now they should pass!
Reading HTTP Stubs from the Classpath with Spring Cloud Contract Stub Runner
Important
|
This feature is available as of the 1.1.1.RELEASE version. |
This part assumes that you have done the previous task so that your consumer project is properly set up.
To read the stubs:
-
Open the
BeerControllerClasspathTest
. We use Stub Runner to pick stubs from the classpath. -
Now annotate the class with
@AutoConfigureStubRunner(ids = "com.example:beer-api-producer-restdocs:+:8090", stubsMode = StubRunnerProperties.StubsMode.CLASSPATH)
-
Run the tests and you can see them pass!
For this example, we scan the following locations by default:
* /META-INF/com.example/beer-api-producer-restdocs//.
* /contracts/com.example/beer-api-producer-restdocs//.
* /mappings/com.example/beer-api-producer-restdocs/*/.*
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
Reading Messaging Stubs with the Spring Cloud Contract Stub Runner
Since Rest Docs have nothing to do with messaging, we must use the standard Stub Runner approach:
-
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-restdocs")
to download the latest stubs ofcom.example:beer-api-producer-restdocs
, 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
-
Reading Messaging Stubs from the Classpath with Spring Cloud Contract Stub Runner
Important
|
This feature is available as of the 1.1.1.RELEASE version |
Now that you have written the implementation and have tested it in the previous section,
we can try to read the message stubs from classpath. To do so, annotate the
BeerVerificationListenerClasspathTest
class with
@AutoConfigureStubRunner(ids = "com.example:beer-api-producer-restdocs:+:8090", stubsMode = StubRunnerProperties.StubsMode.LOCAL)
Now you can run the tests and see them tests pass!
For this example, we scan the following locations by default:
* /META-INF/com.example/beer-api-producer-restdocs//.
* /contracts/com.example/beer-api-producer-restdocs//.
* /mappings/com.example/beer-api-producer-restdocs/*/.*
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;
Rest Docs Producer Tests Config
@Configuration
@EnableAutoConfiguration
static class Config {
@Bean
PersonCheckingService personCheckingService() {
return personToCheck -> personToCheck.age >= 20;
}
@Bean
ProducerController producerController(PersonCheckingService service) {
return new ProducerController(service);
}
}
Rest Docs Producer Tests
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ProducerControllerTests.Config.class)
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureMockMvc
@AutoConfigureJsonTesters
@DirtiesContext
public class ProducerControllerTests {
@Autowired private MockMvc mockMvc;
private JacksonTester<PersonToCheck> json;
@BeforeEach
public void setup() {
ObjectMapper objectMappper = new ObjectMapper();
// Possibly configure the mapper
JacksonTester.initFields(this, objectMappper);
}
@Test
public void should_grant_a_beer_when_person_is_old_enough() throws Exception {
PersonToCheck personToCheck = new PersonToCheck(34);
mockMvc.perform(MockMvcRequestBuilders.post("/check")
.contentType(MediaType.APPLICATION_JSON)
.content(json.write(personToCheck).getJson()))
.andExpect(jsonPath("$.status").value("OK"));
}
@Test
public void should_reject_a_beer_when_person_is_too_young() throws Exception {
PersonToCheck personToCheck = new PersonToCheck(10);
mockMvc.perform(MockMvcRequestBuilders.post("/check")
.contentType(MediaType.APPLICATION_JSON)
.content(json.write(personToCheck).getJson()))
.andExpect(jsonPath("$.status").value("NOT_OK"));
}
@Configuration
@EnableAutoConfiguration
static class Config {
@Bean
PersonCheckingService personCheckingService() {
return personToCheck -> personToCheck.age >= 20;
}
@Bean
ProducerController producerController(PersonCheckingService service) {
return new ProducerController(service);
}
}
}
Rest Docs Producer Tests with Contracts
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ProducerControllerTests.Config.class)
@AutoConfigureRestDocs(outputDir = "target/snippets")
@AutoConfigureMockMvc
@AutoConfigureJsonTesters
@DirtiesContext
public class ProducerControllerTests {
@Autowired private MockMvc mockMvc;
private JacksonTester<PersonToCheck> json;
@BeforeEach
public void setup() {
ObjectMapper objectMappper = new ObjectMapper();
// Possibly configure the mapper
JacksonTester.initFields(this, objectMappper);
}
@Test
public void should_grant_a_beer_when_person_is_old_enough() throws Exception {
PersonToCheck personToCheck = new PersonToCheck(34);
mockMvc.perform(MockMvcRequestBuilders.post("/check")
.contentType(MediaType.APPLICATION_JSON)
.content(json.write(personToCheck).getJson()))
.andExpect(jsonPath("$.status").value("OK"))
.andDo(WireMockRestDocs.verify()
.jsonPath("$[?(@.age >= 20)]")
.contentType(MediaType.valueOf("application/json"))
.stub("shouldGrantABeerIfOldEnough"))
.andDo(MockMvcRestDocumentation.document("shouldGrantABeerIfOldEnough",
SpringCloudContractRestDocs.dslContract()));
}
@Test
public void should_reject_a_beer_when_person_is_too_young() throws Exception {
PersonToCheck personToCheck = new PersonToCheck(10);
mockMvc.perform(MockMvcRequestBuilders.post("/check")
.contentType(MediaType.APPLICATION_JSON)
.content(json.write(personToCheck).getJson()))
.andExpect(jsonPath("$.status").value("NOT_OK"))
.andDo(WireMockRestDocs.verify()
.jsonPath("$[?(@.age < 20)]")
.contentType(MediaType.valueOf("application/json"))
.stub("shouldRejectABeerIfTooYoung"))
.andDo(MockMvcRestDocumentation.document("shouldRejectABeerIfTooYoung",
SpringCloudContractRestDocs.dslContract()));
}
@Configuration
@EnableAutoConfiguration
static class Config {
@Bean
PersonCheckingService personCheckingService() {
return personToCheck -> personToCheck.age >= 20;
}
@Bean
ProducerController producerController(PersonCheckingService service) {
return new ProducerController(service);
}
}
}