In this tutorial, we keep the contracts together with the producer code. Each consumer defines the contracts in a dedicated folder. For the same requests, the consumers expect different responses.
Scenarios
We must write the following features:




Flow

Tutorial
Consumer Driven Contracts are like TDD in terms of architecture. We start by writing a test on the consumer side. This time, we simulate in our single code base as if we had two separate consumers. Normally, both consumers would have their own code bases, but, for the sake of demonstration, we try to keep things simple.
Consumer flow 1

IDE setup
-
In your IDE, open the
consumer_with_stubs_per_consumer
project (either via Maven or Gradle) -
We have the following objectives for HTTP:
-
As a consumer with name
foo-service
:-
For a client with a certain
name
andage
, we ask the producer to verify whether the person is eligible to get the beer. -
We expect to retrieve the
status
and thename
of the client. -
Depending on the
status
, we either accept or decline giving a beer. -
Since we have a friendly service, we address the client by
MY DEAR FRIEND
and then use the person’sname
. -
If the person (for example, with the name,
marcin
) can get the beer then we sayTHERE YOU GO MY DEAR FRIEND [marcin]
. -
Otherwise, we say
GET LOST MY DEAR FRIEND [marcin]
.
-
-
As a consumer with name
bar-service
:-
For the client with a certain
name
andage
, we ask the producer to verify whether the person is eligible to get the beer. -
We expect to retrieve the
status
and thesurname
of the client (for simplicity we receive the providedname
assurname
). -
Depending on the
status
, we either accept or decline giving a beer. -
Since we have a very official service, we address the client by
MR
. -
If the person (for example, with the name,
marcin
) can get the beer, then we sayTHERE YOU GO MR [marcin]
, Wheremarcin
came back from the producer service as asurname
. -
Otherwise, we say
GET LOST MR [marcin]
.
-
-
-
Normally we would do TDD, but we already have some code ready to speed things up.
-
In the
BeerController
class, you can see that the implementation of the method is missing. We return to that later. For now, you can see that we have a/beer
endpoint that expects a JSON request body that maps to aPerson
class. -
Now open the
BeerControllerTest
and write the missing test bodies.
@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 MY DEAR FRIEND [marcin]"));
//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 MY DEAR FRIEND [marcin]"));
//remove::end[]
}
We need to name our consumer somehow. The best way is to provide that value in the
properties
attribute in a SpringBootTest
annotation, though you also could pass it
via a file, such as application.yml
.
@SpringBootTest(webEnvironment = WebEnvironment.MOCK,
properties = {"spring.application.name=foo-consumer"})
If we run the tests, they fail, because we have no implementation.
Now open BeerControllerForBarTest
. In this test class, we simulate that we are using
the bar-service
and not the foo-service
. We can start with the missing test
implementation.
@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 MR [marcin]"));
//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 MR [marcin]"));
//remove::end[]
}
In this test, we do not set the spring.application.name
. We will change the name with
an attribute in an annotation later.
Now we want to write an implementation, but the problem is that we do not yet know what
API we would like to have. Here, we touch the very essence of Consumer Driven
Contracts. As consumers, we want to drive the change of the API. That is why, as
consumers, we work on the producer code.
Cloning the producer’s code
-
In this tutorial we will not clone the producer’s code, we’ll just open it in the IDE
-
There’s some production code written on the producer side but you could completely remove it. The idea of CDC is that defining of contract can be done without writing a single line of code for the feature.
Adding dependencies in the producer’s clone
-
Since we want the IDE to help us with code completion, let’s add the necessary Spring Cloud Contract dependencies. You need to add
spring-cloud-starter-contract-verifier
as a test dependencyMaven<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-contract-verifier</artifactId> <scope>test</scope> </dependency>
GradletestImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier")
-
This is a task that you would do once only since when you’ll be adding next contracts all the dependencies will already be added
Defining first foo-consumer
HTTP contract
It is time to play with the API Create a src/test/resources/contracts/foo-consumer/rest
folder. You can define the contracts using Groovy DSL. To create your first HTTP
contract:
-
Under the
rest
folder, create a file calledshouldGrantABeerIfOldEnough.groovy
-
Call the
Contract.make
method to start defining the contract.
org.springframework.cloud.contract.spec.Contract.make {
}
You can call description()
method to provide some meaningful description.
Tip
|
You can use the Groovy multiline String """ """ to have all special characters
escaped. Every new line in the String is converted into a new line character, as shown in
the following example:
|
Contract.make {
request {
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
```
""")
}
Now call the request { }
and response { }
methods, as shown in the following example:
org.springframework.cloud.contract.spec.Contract.make {
description("""
some interesting description
""")
request {
}
response {
}
}
Let’s assume that we want to send a POST
method. To do so, call method POST()
or
method "POST"
.
Tip
|
In Groovy, you do not need to provide parentheses (in most cases). You can write
either method POST() or method(POST()) . The result is the same.
|
org.springframework.cloud.contract.spec.Contract.make {
description("""
some interesting description
""")
request {
method POST()
}
response {
}
}
Now we need to provide a URL: /check
. We can write url "/check"
.
org.springframework.cloud.contract.spec.Contract.make {
description("""
some interesting description
""")
request {
method POST()
url "/check"
}
response {
}
}
-
Now we need to define the body. We leverage some of Groovy’s power here, so, if you get lost you can always check the Groovy JSON documentation. Let’s call the
body()
method with brackets. -
In Groovy, you can use the map notation this way:
[key: "value", secondKey: 2]
. In the same way, we can describe the body with JSON. We want to send JSON such as the following{ "age": 22, "name": "marcin" }
. To do so, we can create a map notation of[age:22, name:"marcin"]
. Thebody
method accepts a map. In Groovy, if a method accepts a map, then the[]
brackets can be omitted. So you can write eitherbody([age:22, name:"marcin"])
orbody(age:22, name:"marcin")
. -
Let’s assume that we already want to be more generic about our contract and we want to verify that the age is greater than 20 and that name is any alphaunicode character. We can use the
$()
orvalue()
methods that Spring Cloud Contract provides to define dynamic behaviour. -
We use the
$(regex(…))
method to define the age and the$(anyAlphaUnicode())
for the name.
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: $(regex("[2-9][0-9]")),
name: $(anyAlphaUnicode())
)
}
response {
}
}
Now we can work on the headers, by calling the headers { }
method, as shown in the
following example:
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: $(regex("[2-9][0-9]")),
name: $(anyAlphaUnicode())
)
headers {
}
}
response {
}
}
Inside that method, we want to use the Content-Type: "application/json
header. To do
so, call contentType(applicationJson())
methods, as shown in the following example:
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: $(regex("[2-9][0-9]")),
name: $(anyAlphaUnicode())
)
headers {
contentType(applicationJson())
}
}
response {
}
}
Congratulations! You defined the contract for the request. 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 example:
+
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: $(regex("[2-9][0-9]")),
name: $(anyAlphaUnicode())
)
headers {
contentType(applicationJson())
}
}
response {
status 200
}
}
-
We want our response to have a body. As you might have guessed, there’s a
body
method here, too. We can now use another way of defining bodies by using String. (That is the less preferred option in Spring Cloud Contract, but it can still be useful.) -
We want to send back a field called
status
that will returnOK
when the person can get the beer. Thefoo-consumer
is also interested in getting the name in the response from the request.-
To reference the request from the response via the JSON path, you can call the
fromRequest()
method. In the following code snippet, we reference thename
field from the request: `fromRequest().body('$.name')". -
In Groovy, when you use a multiline string (
""" """
), you can call the${}
interpolation mechanism to call a method from within a String.
-
Tip
|
Don’t confuse the $() from Spring Cloud Contract with the ${} interpolation
mechanism. Call
body(""" { "status" : "OK", "name": "${fromRequest().body('$.name')}" } """) . That way,
you can define how the response body looks by providing the exact JSON value, and inside
that JSON you can also provide dynamic values. In our case, for the name response
field, we provide the value of name from the request, as shown in the following
example:
|
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: $(regex("[2-9][0-9]")),
name: $(anyAlphaUnicode())
)
headers {
contentType(applicationJson())
}
}
response {
status 200
body("""
{
"status" : "OK",
"name": "${fromRequest().body('$.name')}"
}
""")
}
}
The last thing to add is the response headers. We do just about exactly the same thing as
we did previously for the request, as shown here:
headers { contentType(applicationJson()) }
.
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: $(regex("[2-9][0-9]")),
name: $(anyAlphaUnicode())
)
headers {
contentType(applicationJson())
}
}
response {
status 200
body("""
{
"status" : "OK",
"name": "${fromRequest().body('$.name')}"
}
""")
headers {
contentType(applicationJson())
}
}
}
Congratulations! You have created your first contract!
Defining the Second foo-consumer
HTTP Contract
Now it’s time for you to create the second contract. Create a file called
shouldRejectABeerIfTooYoung.groovy
.
If you get lost, look at the solution.
-
Set the
age
in the request to the following regular expression[0-1][0-9]
. -
Update the response body to return a
status
ofNOT_OK
. -
Update the description.
Defining bar-consumer
HTTP contracts
Let’s now move to the bar-consumer
. Create a
src/test/resources/contracts/bar-consumer/rest
folder. We will create very similar
contracts to the foo-consumer
one. The only difference is that the response contains a
surname
field instead of a name
field. Create 2 files named
shouldGrantABeerIfOldEnough.groovy
and shouldRejectABeerIfTooYoung.groovy
and fill
them out or copy from the solution. We have written the
contracts. It is time to publish some stubs!
Setting up the Spring Cloud Contract plugin on the producer side
-
Ok, at this moment we’ve described the API that would be interesting for us, consumers, and most likely will suit our needs. We define those contracts cause we want to have some stubs produced for us without needing to write a single line of the implementation code. The tool that we need to do this conversion is the Spring Cloud Contract plugin. Let’s add it to the producer’s
pom.xml
/build.gradle
.Maven<plugin> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-maven-plugin</artifactId> <version>${spring-cloud-contract.version}</version> <extensions>true</extensions> </plugin>
Gradlebuildscript { dependencies { classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${verifierVersion}" } }
-
The coordinates of the plugin are:
org.springframework.cloud:spring-cloud-contract-gradle-plugin:$2.5.3
-
For this tutorial we’re using latest snapshot versions that you can reference via the Maven’s
${spring-cloud-contract.version}
property or Gradle’sverifierVersion
one -
Once the plugin has been added just call the commands to install the stubs locally
Maven$ ./mvnw clean install -DskipTests
Gradle$ ./gradlew clean build publishToMavenLocal -x test
-
Now you can check out
target/stubs/META-INF/com.example/beer-api-producer-with-stubs-per-consumer/0.0.1-SNAPSHOT
for Maven orbuild/stubs/META-INF/com.example/beer-api-producer-with-stubs-per-consumer/0.0.1-SNAPSHOT
for Gradle. Over there you’ll seecontracts
folder where all contracts got copied and themappings
folder where you’ll find all the generated stubs. By default Spring Cloud Contract uses WireMock as an implementation of fake HTTP server. Under therest
subfolder you’ll see all the generated stubs. Notice that we’re using JSON Paths to check the contents of the request.
-
Writing missing implementation on the consumer side
We know what the API should look like. Let’s go to BeerController
and write the
missing implementation or copy from the solution.
-
We want to send a POST HTTP method to
http://localhost:8090/check
. -
The JSON body will contain the
Person
that we received in the controller. -
Depending on the status (
OK
orNOT_OK
), we send back:-
For
OK
:THERE YOU GO ` + the result of the `message(body of the response)
method. -
For
NOT_OK
:GET LOST ` + the result of the `message(body of the response)
method.
-
If we run the BeerControllerTest
and BeerControllerForBarTest
, they both fail due
to the connection being refused. Let’s fix that.
Turning on Stub Runner in Consumer Tests
Since we managed to install the stubs locally and we now have the missing implementation written, we can now go back to the consumer tests. Let’s add the Spring Cloud Contract Stub Runner as a dependency, as shown in the following example:
+
<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")
Let’s check out the BeerControllerTest
and add the Stub Runner functionality, as shown
in the following example:
@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL,
ids = "com.example:beer-api-producer-with-stubs-per-consumer",
stubsPerConsumer = true)
You can see that we turned on the stubsPerConsumer
flag. Doing so means that the path
of stubs is scanned and only those that contain the value of spring.application.name
is picked. Now let’s run the test. It should pass. Let’s try to fix the
BeerControllerForBarTest
. We do not want to set the spring.application.name
. That’s
why we will set that name on the @AutoConfigureStubRunner
annotation via the
consumerName
property. (Note that we also have to turn on the stubsPerConsumer
flag.)
@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL,
ids = "com.example:beer-api-producer-with-stubs-per-consumer",
stubsPerConsumer = true,
consumerName = "bar-consumer")
Congratulations! As a consumer, you successfully used the API of the producer for both HTTP and messaging. Now we can file a pull request (PR) to their code to propose a 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 our situation, we use the mapping approach. Let’s set the following base classes for our contracts, as shown in the following example:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<baseClassMappings>
<baseClassMapping>
<contractPackageRegex>.*rest.*</contractPackageRegex>
<baseClassFQN>com.example.BeerRestBase</baseClassFQN>
</baseClassMapping>
</baseClassMappings>
</configuration>
</plugin>
contracts {
testFramework = "JUNIT5"
baseClassMappings {
baseClassMapping(".*rest.*", "com.example.BeerRestBase")
}
}
Generating tests from contracts
-
Let’s generate the tests! Just call:
Maven$ ./mvnw clean install
Gradle$ ./gradlew clean build publishToMavenLocal
-
Suddenly some tests should start failing. Those tests are the autogenerated tests created by Spring Cloud Contract
-
The tests lay under
/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/beer
intarget
for Maven orbuild
for Gradle -
There will be a test for each folder in which you store your contracts. The name of the test class will be the name of that folder
-
Each of the contracts will be a single test inside that test class
-
If you check out the generated tests you’ll notice that the dynamic parts of the
request
part of the contract got converted to a concrete value. Any dynamic bits on theresponse
side would be converted into matchers.
-
-
Time to fix the broken tests. We need to do that by providing the missing implementation.
Fixing broken HTTP tests
Let’s start with HTTP. First, let’s write the missing implementation in
ProducerController
. The logic to be written is as follows: If the
personCheckingService.shouldGetBeer(…)
returns true
, then we should return
new Response(BeerCheckStatus.OK, personToCheck.name)
. Otherwise, we should return
new Response(BeerCheckStatus.NOT_OK, personToCheck.name)
.
(Show solution).
-
Let’s fix the
BeerRestBase
class now-
The idea of CDC is NOT TO TEST every single feature. Contract tests are there to see if the API is matched, NOT that the feature is working. That’s why we shouldn’t be accessing databases etc. That means that we will work with mock of the
PersonCheckingService
. (Show solution) -
Let’s annotate the test class with
@RunWith(MockitoJUnitRunner.class)
to enable Mockito runner.@RunWith(MockitoJUnitRunner.class) public abstract class BeerRestBase { ... }
-
We’ll want to test the
ProducerController
so we can create a field@InjectMocks ProducerController producerController
. Mockito will inject any mocks for us via the constructor.@Mock PersonCheckingService personCheckingService; @InjectMocks ProducerController producerController; @BeforeEach public void setup() { given(personCheckingService.shouldGetBeer(argThat(oldEnough()))).willReturn(true); }
-
It won’t compile cause we don’t have the
oldEnough()
method but don’t worry. So this line stubs theshouldGetBeer
method in such a way that if the user is old enough then the method will return true. Let’s now add theoldEnoughMethod()
private TypeSafeMatcher<PersonToCheck> oldEnough() { return new TypeSafeMatcher<PersonToCheck>() { @Override protected boolean matchesSafely(PersonToCheck personToCheck) { return personToCheck.age >= 20; } @Override public void describeTo(Description description) { } }; }
-
We’re using the
TypeSafeMatcher
from Hamcrest to create a matcher forPersonToCheck
. In this case if the person to check is older or is 20 then the methodshouldGetBeer
method will returntrue
. -
Now we need to configure RestAssured that is used by Spring Cloud Contract to send requests. In our case we want to profit from MockMvc. In order to set the
ProducerController
with RestAssured it’s enough to call// https://github.com/spring-cloud/spring-cloud-contract/issues/1428 EncoderConfig encoderConfig = new EncoderConfig().appendDefaultContentCharsetToContentTypeIfUndefined(false); RestAssuredMockMvc.config = new RestAssuredMockMvcConfig().encoderConfig(encoderConfig); RestAssuredMockMvc.standaloneSetup(producerController);
@RunWith(MockitoJUnitRunner.class) public abstract class BeerRestBase { @Mock PersonCheckingService personCheckingService; @InjectMocks ProducerController producerController; @BeforeEach public void setup() { given(personCheckingService.shouldGetBeer(argThat(oldEnough()))).willReturn(true); // https://github.com/spring-cloud/spring-cloud-contract/issues/1428 EncoderConfig encoderConfig = new EncoderConfig().appendDefaultContentCharsetToContentTypeIfUndefined(false); RestAssuredMockMvc.config = new RestAssuredMockMvcConfig().encoderConfig(encoderConfig); RestAssuredMockMvc.standaloneSetup(producerController); } private TypeSafeMatcher<PersonToCheck> oldEnough() { return new TypeSafeMatcher<PersonToCheck>() { @Override protected boolean matchesSafely(PersonToCheck personToCheck) { return personToCheck.age >= 20; } @Override public void describeTo(Description description) { } }; } }
-
With mocks and RestAssured setup - we’re ready to run our HTTP based autogenerated tests
-
Now you can merge the pull request to master
and your CI system can build a fat jar and
the stubs.
Important
|
Per consumer stubs is a powerful feature. On the producer side, if you want to
remove a field from the response, you can quickly verify if gets used. Try removing
the surname field from the Response class. You can see that the generated RestTest
in the bar-consumer subfolder fails. That means that the bar-consumer requires the
surname field and that you can’t safely remove it. On the other hand, in production,
both consumers receive more fields than they define in the contract. Thus, if they do not
configure their serializers properly (to ignore unknown fields), then their tests pass
but their integrations fail.
|
Congratulations! You have completed the producer side of this tutorial.
Consumer flow 2

-
After merging the PR the producer’s stubs reside in some Artifactory / Nexus instance
-
As consumers we no longer want to retrieve the stubs from our local Maven repository - we’d like to download them from the remote location
-
To do that (we won’t do that for the tutorial but you would do it in your "production" code) it’s enough to pass the
repositoryRoot
parameter and set thestubsMode
toStubRunnerProperties.StubsMode.REMOTE
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = WebEnvironment.MOCK) @AutoConfigureMockMvc @AutoConfigureJsonTesters @AutoConfigureStubRunner( repositoryRoot="http://www.foo.com/bar, ids = "com.example:beer-api-producer-with-stubs-per-consumer:+:stubs:8090", stubsMode = StubRunnerProperties.StubsMode.REMOTE ) @DirtiesContext public class YourTestOnTheConsumerSide extends AbstractTest { }
-
Another option is to pass the property
stubrunner.repositoryRoot
either as a system / environment property, or via anapplication.yml
andstubrunner.stubs-mode
equal toremote
-
Generating documentation from contracts
Another feature of Spring Cloud Contract is an option to easily create the documentation
of the whole API of the producer. You can create the following test that will generate a
contracts.adoc
file under target/generated-snippets/
with description of contracts and
with the contract bodies as such.
Tip
|
This test is a poor man’s version of the documentation generation. You can customize it as you wish - the current version is just to show you an example. |
package docs;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter;
import org.springframework.core.io.Resource;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collection;
import java.util.regex.Pattern;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig
public class GenerateAdocsFromContractsTests {
// TODO: Can be parametrized
@Value("classpath:contracts") Resource contracts;
private static String header = "= Application Contracts\n" + "\n"
+ "In the following document you'll be able to see all the contracts that are present for this application.\n"
+ "\n" + "== Contracts\n";
@Test public void should_convert_contracts_into_adoc() throws IOException {
final StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(header);
final Path rootDir = this.contracts.getFile().toPath();
Files.walkFileTree(rootDir, new FileVisitor<Path>() {
private Pattern pattern = Pattern.compile("^.*groovy$");
@Override
public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes atts)
throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes mainAtts)
throws IOException {
boolean matches = this.pattern.matcher(path.toString()).matches();
if (matches) {
appendContract(stringBuilder, path);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path path, IOException exc)
throws IOException {
return FileVisitResult.CONTINUE;
}
@Override public FileVisitResult visitFileFailed(Path path, IOException exc)
throws IOException {
// If the root directory has failed it makes no sense to continue
return path.equals(rootDir) ?
FileVisitResult.TERMINATE :
FileVisitResult.CONTINUE;
}
});
//String outputAdoc = asciidoctor.convert(stringBuilder.toString(), new HashMap<String, Object>());
String outputAdoc = stringBuilder.toString();
// TODO: Can be parametrized
File outputDir = new File("target/generated-snippets");
outputDir.mkdirs();
// TODO: Can be parametrized
File outputFile = new File(outputDir, "contracts.adoc");
if (outputFile.exists()) {
outputFile.delete();
}
if (outputFile.createNewFile()) {
Files.write(outputFile.toPath(), outputAdoc.getBytes());
}
}
static StringBuilder appendContract(final StringBuilder stringBuilder, Path path)
throws IOException {
Collection<Contract> contracts = ContractVerifierDslConverter.convertAsCollection(path.getParent().toFile(), path.toFile());
// TODO: Can be parametrized
contracts.forEach(contract -> {
stringBuilder.append("### ")
.append(path.getFileName().toString())
.append("\n\n")
.append(contract.getDescription())
.append("\n\n")
.append("#### Contract structure")
.append("\n\n")
.append("[source,java,indent=0]")
.append("\n")
.append("----")
.append("\n")
.append(fileAsString(path))
.append("\n")
.append("----")
.append("\n\n");
});
return stringBuilder;
}
static String fileAsString(Path path) {
try {
byte[] encoded = Files.readAllBytes(path);
return new String(encoded, StandardCharsets.UTF_8);
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Solutions
Written consumer tests
@Test
public void should_give_me_a_beer_when_im_old_enough() throws Exception {
//remove::start[]
this.mockMvc.perform(MockMvcRequestBuilders.post("/beer")
.contentType(MediaType.APPLICATION_JSON)
.content(this.json.write(new Person("marcin", 22)).getJson()))
.andExpect(status().isOk())
.andExpect(content().string("THERE YOU GO"));
//remove::end[]
}
@Test
public void should_reject_a_beer_when_im_too_young() throws Exception {
//remove::start[]
this.mockMvc.perform(MockMvcRequestBuilders.post("/beer")
.contentType(MediaType.APPLICATION_JSON)
.content(this.json.write(new Person("marcin", 17)).getJson()))
.andExpect(status().isOk())
.andExpect(content().string("GET LOST"));
//remove::end[]
}
Adding Spring Cloud Contract Dependency
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier")
Proposal of simple contracts by consumer
HTTP communication
// rest/shouldGrantABeerIfOldEnough.groovy
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a successful scenario of getting a beer
```
given:
client is old enough
when:
he applies for a beer
then:
we'll grant him the beer
```
""")
request {
method 'POST'
url '/check'
body(
age: 22,
name: "marcin"
)
headers {
contentType(applicationJson())
}
}
response {
status 200
body("""
{
"status": "OK"
}
""")
headers {
contentType(applicationJson())
}
}
}
// rest/shouldRejectABeerIfTooYoung.groovy
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents a successful scenario of getting a beer
```
given:
client is old enough
when:
he applies for a beer
then:
we'll grant him the beer
```
""")
request {
method 'POST'
url '/check'
body(
age: 17,
name: "marcin"
)
headers {
contentType(applicationJson())
}
}
response {
status 200
body("""
{
"status": "NOT_OK"
}
""")
headers {
contentType(applicationJson())
}
}
}
Messaging communication
// messaging/shouldSendAcceptedVerification.groovy
org.springframework.cloud.contract.spec.Contract.make {
description("""
Sends a positive verification message when person is eligible to get the beer
```
given:
client is old enough
when:
he applies for a beer
then:
we'll send a message with a positive verification
```
""")
// Label by means of which the output message can be triggered
label 'accepted_verification'
// output message of the contract
outputMessage {
// destination to which the output message will be sent
sentTo 'verifications'
// the body of the output message
body(
eligible: true
)
headers {
header("contentType", applicationJsonUtf8())
}
}
}
// messaging/shouldSendRejectedVerification.groovy
org.springframework.cloud.contract.spec.Contract.make {
description("""
Sends a negative verification message when person is not eligible to get the beer
```
given:
client is too young
when:
he applies for a beer
then:
we'll send a message with a negative verification
```
""")
// Label by means of which the output message can be triggered
label 'rejected_verification'
// output message of the contract
outputMessage {
// destination to which the output message will be sent
sentTo 'verifications'
// the body of the output message
body(
eligible: false
)
headers {
header("contentType", applicationJsonUtf8())
}
}
}
Missing consumer controller code
ResponseEntity<Response> response = this.restTemplate.exchange(
RequestEntity
.post(URI.create("http://localhost:" + this.port + "/check"))
.contentType(MediaType.APPLICATION_JSON)
.body(person),
Response.class);
switch (response.getBody().status) {
case OK:
return "THERE YOU GO";
default:
return "GET LOST";
}
Stub Logs
2017-05-11 12:16:51.146 INFO 4693 --- [ main] o.s.c.c.s.StubDownloaderBuilderProvider : Will download stubs using Aether
2017-05-11 12:16:51.148 INFO 4693 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Remote repos not passed but the switch to work offline was set. Stubs will be used from your local Maven repository.
2017-05-11 12:16:51.291 INFO 4693 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Desired version is [+] - will try to resolve the latest version
2017-05-11 12:16:51.308 INFO 4693 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved version is [0.0.1-SNAPSHOT]
2017-05-11 12:16:51.309 INFO 4693 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolving artifact [com.example:{producer_artifact}:jar:stubs:0.0.1-SNAPSHOT] using remote repositories []
2017-05-11 12:16:51.317 INFO 4693 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved artifact [com.example:{producer_artifact}:jar:stubs:0.0.1-SNAPSHOT] to /home/marcin/.m2/repository/com/example/{producer_artifact}/0.0.1-SNAPSHOT/{producer_artifact}-0.0.1-SNAPSHOT-stubs.jar
2017-05-11 12:16:51.322 INFO 4693 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Unpacking stub from JAR [URI: file:/home/marcin/.m2/repository/com/example/{producer_artifact}/0.0.1-SNAPSHOT/{producer_artifact}-0.0.1-SNAPSHOT-stubs.jar]
2017-05-11 12:16:51.327 INFO 4693 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Unpacked file to [/tmp/contracts9053257535983128167]
2017-05-11 12:16:52.608 INFO 4693 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@699e0bf0: startup date [Thu May 11 12:16:52 CEST 2017]; root of context hierarchy
2017-05-11 12:16:52.684 INFO 4693 --- [ main] f.a.AutowiredAnnotationBeanPostProcessor : JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2017-05-11 12:16:52.837 INFO 4693 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8090 (http)
2017-05-11 12:16:52.851 INFO 4693 --- [ main] o.apache.catalina.core.StandardService : Starting service Tomcat
2017-05-11 12:16:52.853 INFO 4693 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.14
2017-05-11 12:16:52.975 INFO 4693 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2017-05-11 12:16:52.975 INFO 4693 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 367 ms
2017-05-11 12:16:52.996 INFO 4693 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Mapping servlet: 'stub' to [/]
2017-05-11 12:16:53.000 INFO 4693 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Mapping servlet: 'admin' to [/__admin/*]
2017-05-11 12:16:53.135 INFO 4693 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8090 (http)
2017-05-11 12:16:53.139 INFO 4693 --- [ main] o.s.c.contract.stubrunner.StubServer : Started stub server for project [com.example:{producer_artifact}:0.0.1-SNAPSHOT:stubs] on port 8090
Beer Request
class BeerRequest {
public int age;
public BeerRequest(int age) {
this.age = age;
}
public BeerRequest() {
}
}
Missing listener code
if (verification.eligible) {
this.eligibleCounter.incrementAndGet();
} else {
this.notEligibleCounter.incrementAndGet();
}
Missing triggers
@Test public void should_increase_the_eligible_counter_when_verification_was_accepted() throws Exception {
int initialCounter = this.listener.eligibleCounter.get();
//remove::start[]
this.stubTrigger.trigger("accepted_verification");
//remove::end[]
then(this.listener.eligibleCounter.get()).isGreaterThan(initialCounter);
}
@Test public void should_increase_the_noteligible_counter_when_verification_was_rejected() throws Exception {
int initialCounter = this.listener.notEligibleCounter.get();
//remove::start[]
this.stubTrigger.trigger("rejected_verification");
//remove::end[]
then(this.listener.notEligibleCounter.get()).isGreaterThan(initialCounter);
}
Messaging DSLs
// messaging/shouldSendAcceptedVerification.groovy
org.springframework.cloud.contract.spec.Contract.make {
description("""
Sends a positive verification message when person is eligible to get the beer
```
given:
client is old enough
when:
he applies for a beer
then:
we'll send a message with a positive verification
```
""")
// Label by means of which the output message can be triggered
label 'accepted_verification'
// input to the contract
input {
// the contract will be triggered by a method
triggeredBy('clientIsOldEnough()')
}
// output message of the contract
outputMessage {
// destination to which the output message will be sent
sentTo 'verifications'
// the body of the output message
body(
eligible: true
)
headers {
header("contentType", applicationJsonUtf8())
}
}
}
// messaging/shouldSendRejectedVerification.groovy
org.springframework.cloud.contract.spec.Contract.make {
description("""
Sends a negative verification message when person is not eligible to get the beer
```
given:
client is too young
when:
he applies for a beer
then:
we'll send a message with a negative verification
```
""")
// Label by means of which the output message can be triggered
label 'rejected_verification'
// input to the contract
input {
// the contract will be triggered by a method
triggeredBy('clientIsTooYoung()')
}
// output message of the contract
outputMessage {
// destination to which the output message will be sent
sentTo 'verifications'
// the body of the output message
body(
eligible: false
)
headers {
header("contentType", applicationJsonUtf8())
}
}
}
ProducerController implementation
if (personCheckingService.shouldGetBeer(personToCheck)) {
return new Response(BeerCheckStatus.OK);
}
return new Response(BeerCheckStatus.NOT_OK);
BeerRestBase
@RunWith(MockitoJUnitRunner.class)
public abstract class BeerRestBase {
@Mock PersonCheckingService personCheckingService;
@InjectMocks ProducerController producerController;
@BeforeEach
public void setup() {
given(personCheckingService.shouldGetBeer(argThat(oldEnough()))).willReturn(true);
// https://github.com/spring-cloud/spring-cloud-contract/issues/1428
EncoderConfig encoderConfig = new EncoderConfig().appendDefaultContentCharsetToContentTypeIfUndefined(false);
RestAssuredMockMvc.config = new RestAssuredMockMvcConfig().encoderConfig(encoderConfig);
RestAssuredMockMvc.standaloneSetup(producerController);
}
private TypeSafeMatcher<PersonToCheck> oldEnough() {
return new TypeSafeMatcher<PersonToCheck>() {
@Override protected boolean matchesSafely(PersonToCheck personToCheck) {
return personToCheck.age >= 20;
}
@Override public void describeTo(Description description) {
}
};
}
}
BeerMessagingBase
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ProducerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
@AutoConfigureMessageVerifier
@ImportAutoConfiguration(TestChannelBinderConfiguration.class)
public abstract class BeerMessagingBase {
@Inject MessageVerifier messaging;
@Autowired PersonCheckingService personCheckingService;
@BeforeEach
public void setup() {
// let's clear any remaining messages
// output == destination or channel name
this.messaging.receive("output", 100, TimeUnit.MILLISECONDS);
}
public void clientIsOldEnough() {
personCheckingService.shouldGetBeer(new PersonToCheck(25));
}
public void clientIsTooYoung() {
personCheckingService.shouldGetBeer(new PersonToCheck(5));
}
}
Messaging implementation
boolean shouldGetBeer = personToCheck.age >= 20;
this.source.send("output-out-0", new Verification(shouldGetBeer));
return shouldGetBeer;
Foo-consumer contracts
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: $(regex("[2-9][0-9]")),
name: $(anyAlphaUnicode())
)
headers {
contentType(applicationJson())
}
}
response {
status 200
body("""
{
"status" : "OK",
"name": "${fromRequest().body('$.name')}"
}
""")
headers {
contentType(applicationJson())
}
}
}
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents an unsuccessful scenario of getting a beer
```
given:
client is too young
when:
he applies for a beer
then:
we'll NOT grant him the beer
```
""")
request {
method POST()
url "/check"
body(
age: $(regex("[0-1][0-9]")),
name: $(anyAlphaUnicode())
)
headers {
contentType(applicationJson())
}
}
response {
status 200
body("""
{
"status" : "NOT_OK",
"name": "${fromRequest().body('$.name')}"
}
""")
headers {
contentType(applicationJson())
}
}
}
Bar-consumer contracts
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: $(regex("[2-9][0-9]")),
name: $(anyAlphaUnicode())
)
headers {
contentType(applicationJson())
}
}
response {
status 200
body("""
{
"status" : "OK",
"surname": "${fromRequest().body('$.name')}"
}
""")
headers {
contentType(applicationJson())
}
}
}
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents an unsuccessful scenario of getting a beer
```
given:
client is too young
when:
he applies for a beer
then:
we'll NOT grant him the beer
```
""")
request {
method POST()
url "/check"
body(
age: $(regex("[0-1][0-9]")),
name: $(anyAlphaUnicode())
)
headers {
contentType(applicationJson())
}
}
response {
status 200
body("""
{
"status" : "NOT_OK",
"surname": "${fromRequest().body('$.name')}"
}
""")
headers {
contentType(applicationJson())
}
}
}
Stubs Per Consumer BeerController
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 " + message(response.getBody());
default:
return "GET LOST " + message(response.getBody());
}
ProducerController for stubs per consumer implementation
if (personCheckingService.shouldGetBeer(personToCheck)) {
return new Response(BeerCheckStatus.OK, personToCheck.name);
}
return new Response(BeerCheckStatus.NOT_OK, personToCheck.name);