In this tutorial, we keep the contracts together with the producer code, and we check out the more advanced concepts behind Spring Cloud Contract.
Scenarios
We’ll try to code the following scenario:


Flow

Tutorial
Using Consumer Driven Contracts is like using TDD at the architecture level. We start by writing a test on the consumer side.
Consumer Flow 1

Important
|
This tutorial assumes that you completed the previous tutorials and that the consumer code has already been set up with appropriate dependencies. |
Let’s open the GrumpyBartenderControllerTest
. The first step is to write the missing
implementation of the tests. Basing on the previously shown requirements, the controller
should resemble the following:
@Test public void should_fail_to_sell_beer() throws Exception {
//remove::start[]
this.mockMvc.perform(MockMvcRequestBuilders.post("/grumpy")
.contentType(MediaType.APPLICATION_JSON)
.content(this.json.write(new Person("marcin", 22)).getJson()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.whatTheBartenderSaid").value("You're drunk [marcin]. Go home!"))
.andExpect(jsonPath("$.whatDoWeDo").value("Go to another bar"));
//remove::end[]
}
@Test public void should_sell_beer_to_Josh() throws Exception {
//remove::start[]
this.mockMvc.perform(MockMvcRequestBuilders.post("/grumpy")
.contentType(MediaType.APPLICATION_JSON)
.content(this.json.write(new Person("starbuxman", 22)).getJson()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.whatTheBartenderSaid").value("There you go Josh!"))
.andExpect(jsonPath("$.whatDoWeDo").value("Enjoy!"));
//remove::end[]
}
If we run the tests now, they fail. Let’s now check out the producer’s code
to define the missing contract. Open the producer_advanced
project.
Cloned Producer
As usual in these tutorials, we do not clone the producer’s code, even though we would do so in a real life scenario.
Now let’s write our contract! You can define the contracts with Groovy DSL. Let’s create our first HTTP contract. To do so:
-
Under
src/test/resources/contracts/beer/rest/
, create ashouldNotSellAnyAlcohol.groovy
file. -
Call the
Contract.make
method to start defining the contract, as follows:
org.springframework.cloud.contract.spec.Contract.make {
}
-
You can call the
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 is converted into a new line character. The
following code shows an example:
|
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
}
Now call the request { }
and response { }
methods, as shown below:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
request {
}
response {
}
}
Let’s assume that we want to send a POST
method. Call method POST()
or method "POST"
.
Tip
|
In Groovy, you don’t need to provide parentheses (in most cases). You can write
either method POST() or method(POST()) . In both cases, the effet is the same.
|
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
request {
method POST()
}
response {
}
}
Now we need to provide a URL. You can set it to /buy
by writing url "/buy"
, as shown
in the following code:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
request {
method POST()
url "/buy"
}
response {
}
}
Now it’s time to define the body. We want to define age
and name
fields. Let’s make
name
accept any alpha unicode value. Let’s make age
be a concrete value at this time.
We will make it dynamic in the next step. The following code sets our initial values for
both fields:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
request {
method POST()
url "/check"
body(
name: $(anyAlphaUnicode()),
age: 25
)
}
response {
}
}
As you can see, we used the $()
method where we’ve used the anyAlphaUnicode()
function to set a dynamic value.
Now we can make the age
field be dynamic. In Spring Cloud Contract, you can either
provide the dynamic values directly in the body (as we have with the name
) or via
stubMatchers
and testMatchers
sections. Let’s use the first one. We define the JSON
path for which we want to define a dynamic value, as follows:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
request {
method POST()
url "/check"
body(
name: $(anyAlphaUnicode()),
age: 25
)
}
stubMatchers {
jsonPath('$.age', byRegex('[2-9][0-9]'))
}
response {
}
}
Inside the stubMatchers
section, we defined that an element matching a JSON path of
$.age
has to match a regular expression of [2-9][0-9]
. You can see that there are
methods other than byRegex
. You can read about them in the
documentation.
Important
|
If you provide a value via the matchers section, then the value for the key for which you added the matching is removed from automatic test assertion generation. You have to provide those values manually via the matchers section. |
Now we can create the headers by calling the headers { }
method, as shown in the
following code:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
request {
method POST()
url "/check"
body(
name: $(anyAlphaUnicode()),
age: 25
)
}
stubMatchers {
jsonPath('$.age', byRegex('[2-9][0-9]'))
}
headers {
}
}
response {
}
}
Inside that method, let’s define that we want to use the
Content-Type: "application/json"
header. Just call contentType(applicationJson())
methods:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
request {
method POST()
url "/check"
body(
name: $(anyAlphaUnicode()),
age: 25
)
}
stubMatchers {
jsonPath('$.age', byRegex('[2-9][0-9]'))
}
headers {
contentType(applicationJson())
}
}
response {
}
}
Congratulations! You defined how you would like the contract for the request to look!
Now we can work on the response. In the response
block, we want to define that the
status of our response will be 200. To do so, call status 200
, as shown in the
following code:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
request {
method POST()
url "/check"
body(
name: $(anyAlphaUnicode()),
age: 25
)
}
stubMatchers {
jsonPath('$.age', byRegex('[2-9][0-9]'))
}
headers {
contentType(applicationJson())
}
}
response {
status 200
}
}
We want our response to have a body. We want to use the message
and status
fields.
In the message
, we want to respond with You’re drunk [name from request]. Go home!
For the status
, we want to always return a NOT_OK
value and have a custom assertion
in our tests.
Let’s start with the message
field. Spring Cloud Contract gives you a method, called
fromRequest()
, that lets you specify in the response that you would like to fetch some
values from the request. In our case, the value we want in the request is inside the
request body under the $.name
JSON path. Consequently, we can set the value of
message
to "You’re drunk [${fromRequest().body('$.name')}]. Go home!",
. Note that we
have a ""
Groovy String with a ${}
String interpolation in which we’re calling the
fromRequest()
method. The following code shows how all of that works:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
request {
method POST()
url "/check"
body(
name: $(anyAlphaUnicode()),
age: 25
)
}
stubMatchers {
jsonPath('$.age', byRegex('[2-9][0-9]'))
}
headers {
contentType(applicationJson())
}
}
response {
status 200
body(
message: "You're drunk [${fromRequest().body('$.name')}]. Go home!"
)
}
}
Now we have the message response that references the request. It’s time for the second
field. Until now, we always provided a single value for the dynamic parts of the
contract. Whenever we have dynamic values on one side (consumer or producer), then we
must have a concrete value on the other side. In this case, we can provide that value
manually. For the response in the stub, we need to provide a concrete value equal to
NOT_OK
. For the generated test, we want to have a custom assertion done via the
assertStatus()
method defined in the base class. To achieve that, we need to write
$(c("NOT_OK"), p(execute('assertStatus($it)')))
. Let’s now analyze this syntax:
-
c()
is a shortcut forconsumer()
, andp()
is short forproducer(). By calling `$(c(),p())
, we provide a concrete value for the consumer and a dynamic one for the producer. -
The
$(c("NOT_OK"),…)
means that, for the response in the stub, for thestatus
field, we want the stub to contain a value ofNOT_OK
. -
The
$(…,p(execute('assertStatus($it)')))
means that we want, on the producer side, in the autogenerated tests, to run a method defined in the base class. That method is calledassertStatus()
. As a parameter of that method, we want to pass the value of an element present in the response JSON. In our case, we provide a dynamic value for the$.status
field.assertStatus($it)
gets translated toassertStatus(read the $.status from the response JSON)
Now we can write the response body, as shown in the following code:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
request {
method POST()
url "/check"
body(
name: $(anyAlphaUnicode()),
age: 25
)
}
stubMatchers {
jsonPath('$.age', byRegex('[2-9][0-9]'))
}
headers {
contentType(applicationJson())
}
}
response {
status 200
body(
message: "You're drunk [${fromRequest().body('$.name')}]. Go home!",
status: $(c("NOT_OK"), p(execute('assertStatus($it)')))
)
}
}
Now we may want to perform some more complex analysis of the message
field through a
method called assertMessage()
. There’s another way to do that: We can call the
testMatchers
section.
Under testMatchers
, we can define, through a JSON path, the element we want to
dynamically assert. To do so, we can add the following section:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
request {
method POST()
url "/check"
body(
name: $(anyAlphaUnicode()),
age: 25
)
}
stubMatchers {
jsonPath('$.age', byRegex('[2-9][0-9]'))
}
headers {
contentType(applicationJson())
}
}
response {
status 200
body(
message: "You're drunk [${fromRequest().body('$.name')}]. Go home!",
status: $(c("NOT_OK"), p(execute('assertStatus($it)')))
)
testMatchers {
jsonPath('$.message', byCommand('assertMessage($it)'))
}
}
}
The last thing to add are the response headers. We do exactly the same thing as we
previously did for the request, except that we use
headers { contentType(applicationJson()) }
, as shown in the following example:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
request {
method POST()
url "/check"
body(
name: $(anyAlphaUnicode()),
age: 25
)
}
stubMatchers {
jsonPath('$.age', byRegex('[2-9][0-9]'))
}
headers {
contentType(applicationJson())
}
}
response {
status 200
body(
message: "You're drunk [${fromRequest().body('$.name')}]. Go home!",
status: $(c("NOT_OK"), p(execute('assertStatus($it)')))
)
testMatchers {
jsonPath('$.message', byCommand('assertMessage($it)'))
}
headers {
contentType(applicationJson())
}
}
}
We’re almost done. We wrote a very generic example that catches a person with any name.
However, in the requirements, we saw that a starbuxman
person always has to get the
beer. Consequently, we need a specific case of our generic pattern. That is why we will
set the priority
to 100
(the higher the number, the lower the priority).
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
request {
method POST()
url "/check"
body(
name: $(anyAlphaUnicode()),
age: 25
)
}
stubMatchers {
jsonPath('$.age', byRegex('[2-9][0-9]'))
}
headers {
contentType(applicationJson())
}
}
response {
status 200
body(
message: "You're drunk [${fromRequest().body('$.name')}]. Go home!",
status: $(c("NOT_OK"), p(execute('assertStatus($it)')))
)
testMatchers {
jsonPath('$.message', byCommand('assertMessage($it)'))
}
headers {
contentType(applicationJson())
}
}
priority 100
}
Congratulations! You have created your first contract! Now we can define a version for someone named Josh.
-
Copy the file and call it
shouldSellAlcoholOnlyToStarbuxman.groovy
. -
Set the name to
starbuxman
. -
Set the priority to
10
. -
Set the
message
toThere you go Josh!
. -
Set the
status
toOK
.
Note
|
We do not want to do any custom server-side assertions. |
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
request {
method POST()
url "/check"
body(
name: "starbuxman",
age: 25
)
}
stubMatchers {
jsonPath('$.age', byRegex('[2-9][0-9]'))
}
headers {
contentType(applicationJson())
}
}
response {
status 200
body(
message: "There you go Josh!",
status: "OK"
)
headers {
contentType(applicationJson())
}
}
priority 10
}
Congratulations! You have created all the contracts. Now we can go ahead and install the stubs, as shown in the following code for both Maven and Gradle:
./mvnw clean install -DskipTests
./gradlew clean build publishToMavenLocal
Now we can go back to our consumer tests and open the GrumpyBartenderControllerTest
class. We want to add Stub Runner as usual. However, this time, we do not pass the port.
We want the port to be set automatically.
@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL, ids = "com.example:beer-api-producer-advanced")
How can you retrieve the port value then? There are two ways of doing so:
-
Autowire the field annotated with
@Value("${stubrunner.runningstubs.artifactid.port}")
. -
Use the autowired
StubFinder
interface, which includes thefindStub
method.
The following code shows an example of autowiring the field for
beer-api-producer-advanced
:
@Value("${stubrunner.runningstubs.beer-api-producer-advanced.port}") int stubPort;
@BeforeEach
public void setupPort() {
controller.port = stubPort;
}
The following code shows an example of using the StubFinder
interface for
beer-api-producer-advanced
:
@Autowired StubFinder stubFinder;
@BeforeEach
public void setupPort() {
controller.port = stubFinder.findStubUrl("beer-api-producer-advanced").getPort();
}
Let’s go with the first option (autowiring the field). We want to inject the value of the port of the running stub into the controller. Now, when we run the tests, then they should pass successfully.
Congratulations! As consumers, we successfully used the API of the producer for both HTTP and messaging. Now we can file a pull request (PR) to the producer’s code with the proposal of the contract. Let’s switch to the producer side.
Producer Flow 1

IDE setup
-
Open in your IDE the
producer
project (either via Maven or Gradle) -
We’re assuming that we’ve taken over the PR. Example of how to achieve that in "real life" for a PR that got submitted to via a branch called
the_pr
looks like this:
git fetch origin
git checkout -b the_pr origin/the_pr
git merge master
-
The idea of Spring Cloud Contract is about stub and contract validity. Right now we have a set of contracts defined but we haven’t tested it against the producer side. Time to change that!
Setting up the Spring Cloud Contract plugin
-
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 3 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 (takes 2 last package names and appends
Base
. Having a contractsrc/test/resources/contracts/foo/bar/shouldDoSth.groovy
would create a test class calledBarTest
that would extendFooBarBase
class. -
manual mapping (you can state that contracts matching certain regular expression will have to have a base class with fully qualified name equal to X)
-
-
In the following example we’ll play with convention based naming
-
For Maven under the plugin setup you have to set up the plugin configuration
<configuration><packageWithBaseClasses>com.example</packageWithBaseClasses></configuration>
Maven<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>
Gradlecontracts { testFramework = "JUNIT5" packageWithBaseClasses = 'com.example' }
-
In both cases passing of that value tells the plugin that a given base class is available under the
com.example
package
-
-
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
The situation is that the generated tests have failed. We need to fix it by providing the missing assert methods in the base class. We also need to set up RestAssured. Let’s start with the first one:
-
Go to
BeerRestBase
. -
Add the missing
assertStatus(String status)
andassertMessage(String message)
methods (or copy from the solution).-
assertStatus
should assert that thestatus
is equal toNOT_OK
. -
assertMessage
should assert that themessage
containsGo home!
-
-
Add the missing Rest Assured setup by adding the
BuyController
into the list of standalone set up classes, as shown in the following code snippet:
// https://github.com/spring-cloud/spring-cloud-contract/issues/1428
EncoderConfig encoderConfig = new EncoderConfig().appendDefaultContentCharsetToContentTypeIfUndefined(false);
RestAssuredMockMvc.config = new RestAssuredMockMvcConfig().encoderConfig(encoderConfig);
RestAssuredMockMvc.standaloneSetup(..., new BuyController(), ...);
This time, we are not mocking in the base class, because we already did so in the previous tutorials. Instead, we try to make our tests pass as fast as possible.
Important
|
We want our Controller to use the async servlet functionality. That’s
why we need it to return a Callable<Response>
|
To ensure that we return a Callable<Response>
, we need to write our implementation of
the controller (or copy from the solution) such that it has the
following characteristics:
-
A POST method to the
/buy
endpoint that produces and consumes JSON -
If the
Person
name is equal tostarbuxman
, return astatus
code ofOK
and amessage
ofThere you go Josh
. -
If the
Person
name is not equal tostarbuxman
, return astatus
code ofNOT_OK
and amessage
ofYou’re drunk [name]. Go home!
Now, when you run the build again, your autogenerated tests still fail. The reason for
the failure is that we used the async servlet feature but Rest Assured does not know
that. To fix that, we need to add the async()
method in the response side of our
contracts. (Check the solution). Now, when you run the build again,
your tests should pass.
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;
BuyController
@PostMapping(value = "/buy",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
Callable<Response> buy(@RequestBody Person person) {
if ("starbuxman".equalsIgnoreCase(person.name)) {
return () -> new Response(Status.OK, "There you go Josh!");
}
return () ->new Response(Status.NOT_OK, "You're drunk [" + person.name + "]. Go home!");
}
Missing assert methods
protected void assertStatus(String status) {
BDDAssertions.then(status).isEqualToIgnoringCase(Status.NOT_OK.name());
}
protected void assertMessage(String message) {
BDDAssertions.then(message).contains("Go home!");
}
Grumpy contracts
// rest/shouldNotSellAnyAlcohol.groovy
package contracts.beer.rest
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
request {
method POST()
url '/buy'
body(
name: $(anyAlphaUnicode()),
age: 25
)
bodyMatchers {
jsonPath('$.age', byRegex('[2-9][0-9]'))
}
headers {
contentType(applicationJson())
}
}
response {
status 200
body(
message: "You're drunk [${fromRequest().body('$.name')}]. Go home!",
status: $(c("NOT_OK"), p(execute('assertStatus($it)')))
)
bodyMatchers {
jsonPath('$.message', byCommand('assertMessage($it)'))
}
headers {
contentType(applicationJson())
}
async()
}
priority 100
}
// rest/shouldSellAlcoholToStarbuxman.groovy
package contracts.beer.rest
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description("""
Represents a grumpy waiter that will sell alcohol only to Starbuxman.
""")
request {
method POST()
url '/buy'
body(
name: "starbuxman",
age: 25
)
bodyMatchers {
jsonPath('$.age', byRegex('[2-9][0-9]'))
}
headers {
contentType(applicationJson())
}
}
response {
status 200
body(
message: "There you go Josh!",
status: "OK"
)
headers {
contentType(applicationJson())
}
async()
}
priority 10
}