In this tutorial, we keep the contracts together with the producer code, and we check out the more advanced concepts behind Spring Cloud Contract.
Using Consumer Driven Contracts is like using TDD at the architecture level. We start by writing a test on the consumer side.
|
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:
link:../../consumer/src/test/java/com/example/GrumpyBartenderControllerTest.java[role=include]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.
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.groovyfile. -
Call the
Contract.makemethod 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 thestatusfield, 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$.statusfield.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
messagetoThere you go Josh!. -
Set the
statustoOK.
|
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 publishToMavenLocalNow 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.
link:../../consumer/src/test/java/com/example/GrumpyBartenderControllerTest.java[role=include]How can you retrieve the port value then? There are two ways of doing so:
-
Autowire the field annotated with
@Value("${spring.cloud.contract.stubrunner.runningstubs.artifactid.port}"). -
Use the autowired
StubFinderinterface, which includes thefindStubmethod.
The following code shows an example of autowiring the field for
beer-api-producer-advanced:
@Value("${spring.cloud.contract.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.
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).-
assertStatusshould assert that thestatusis equal toNOT_OK. -
assertMessageshould assert that themessagecontainsGo home!
-
-
Add the missing Rest Assured setup by adding the
BuyControllerinto 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
/buyendpoint that produces and consumes JSON -
If the
Personname is equal tostarbuxman, return astatuscode ofOKand amessageofThere you go Josh. -
If the
Personname is not equal tostarbuxman, return astatuscode ofNOT_OKand amessageofYou’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.
link:../../producer_advanced/src/main/java/com/example/BuyController.java[role=include]link:../../producer_advanced/src/test/java/com/example/BeerRestBase.java[role=include]// rest/shouldNotSellAnyAlcohol.groovy
link:../../producer_advanced/src/test/resources/contracts/beer/rest/shouldNotSellAnyAlcohol.groovy[role=include]// rest/shouldSellAlcoholToStarbuxman.groovy
link:../../producer_advanced/src/test/resources/contracts/beer/rest/shouldSellAlcoholOnlyToStarbuxman.groovy[role=include]



