In this tutorial. we keep the contracts together with the producer code. In the contracts, we describe a stateful scenario where the stub needs to have "memory" to know what the previous state was and what the next one should be.
Scenarios

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

IDE setup
In your IDE, open the consumer
project (through either Maven or Gradle).
In this scenario we need to write a test for HTTP communication that meets the following criteria:
-
The consumer gets asked about the current and previous state of intoxication of a given person.
-
The more you drink, the more intoxicated you get.
-
The flow of intoxication states goes
SOBER
→TIPSY
→DRUNK
→WASTED
. -
The consumer asks the producer for a beer for a given person and, in return the information about the state of intoxication is sent back
In the standard CDC process, we would do TDD. However, in this case, you already have
some code ready. The test IntoxicationControllerTest
contains tests of our feature.
In the IntoxicationController
, we need to call an endpoint on the producer side.
Let’s first write our missing test. There’s already a method called
sendARequestAndExpectStatuses
created for us to fill out. We want to use MockMvc
to
send a request to the /wasted
endpoint with the name marcin
in the JSON request body.
As a response, we expect to get the previousStatus
and currentStatus
of intoxication.
this.mockMvc.perform(MockMvcRequestBuilders.post("/wasted")
.contentType(MediaType.APPLICATION_JSON)
.content(this.json.write(new Person("marcin")).getJson()))
.andExpect(status().isOk())
.andExpect(content().json("{\"previousStatus\":\"" + previousStatus.name() +
"\",\"currentStatus\":\"" + currentStatus.name() + "\"}"));
If we run this test, it fails because we did not write an implementation in the
IntoxicationController
. We want to write the implementation, but we do not yet know the
structure of the request. Since we do not yet know how the API should look, we can clone
the producer’s code to experiment with its API.
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 Stateful HTTP Contracts
In the clone of the producer’s code, let’s create a folder called
src/test/resources/contracts/beer/intoxication
. In Spring Cloud Contract, you can
define steps for a given scenario by relying on the naming convention of the files: If
your contract file starts with a number and a _
character, it is assumed to be part of
the scenario. Here are three examples: 1_sober.groovy
, 2_tipsy.groovy
, and
3_drunk.groovy
.
Let’s create those three files and start writing our first scenario. Open the
1_sober.groovy
file. We need to start by calling the
org.springframework.cloud.contract.spec.Contract.make
method.
org.springframework.cloud.contract.spec.Contract.make {
}
Now let’s provide a meaningful description by using the description
method, as shown in
the following code:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents first step of getting fully drunk
given:
you didn't drink anything
when:
you get a beer
then:
you'll be tipsy
""")
}
Next we can define the request
part of the contract, as shown in the following code:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents first step of getting fully drunk
given:
you didn't drink anything
when:
you get a beer
then:
you'll be tipsy
""")
request {
}
}
Let’s assume that we want to send a POST
request to to the /beer
endpoint. To do so,
we might use the following code:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents first step of getting fully drunk
given:
you didn't drink anything
when:
you get a beer
then:
you'll be tipsy
""")
request {
method POST()
url "/beer"
}
}
The body should contain a name
field equal to marcin
. We can use the Groovy map
notation to define it, as shown in the following code:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents first step of getting fully drunk
given:
you didn't drink anything
when:
you get a beer
then:
you'll be tipsy
""")
request {
method POST()
url "/beer"
body(name: "marcin")
}
}
The content type should be application/json
. The following code shows how to set it:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents first step of getting fully drunk
given:
you didn't drink anything
when:
you get a beer
then:
you'll be tipsy
""")
request {
method POST()
url "/beer"
body(name: "marcin")
headers {
contentType(applicationJson())
}
}
}
Congratulations! You successfully defined the request side of the contract. Let’s now
proceed with the response side, by defining the response
block:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents first step of getting fully drunk
given:
you didn't drink anything
when:
you get a beer
then:
you'll be tipsy
""")
request {
method POST()
url "/beer"
body(name: "marcin")
headers {
contentType(applicationJson())
}
}
response {
}
}
We want the response to return status 200
, which we can do with the following code:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents first step of getting fully drunk
given:
you didn't drink anything
when:
you get a beer
then:
you'll be tipsy
""")
request {
method POST()
url "/beer"
body(name: "marcin")
headers {
contentType(applicationJson())
}
}
response {
status 200
}
}
The response body should return a value of SOBER
in the the previousStatus
field and
a value of TIPSY
in the the currentStatus
field. The following code shows how we
might do it:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents first step of getting fully drunk
given:
you didn't drink anything
when:
you get a beer
then:
you'll be tipsy
""")
request {
method POST()
url "/beer"
body(name: "marcin")
headers {
contentType(applicationJson())
}
}
response {
status 200
body(
previousStatus: "SOBER",
currentStatus: "TIPSY"
)
}
}
Finally, the response headers should contain a content type of application/json
, as
shown in the following code:
org.springframework.cloud.contract.spec.Contract.make {
description("""
Represents first step of getting fully drunk
given:
you didn't drink anything
when:
you get a beer
then:
you'll be tipsy
""")
request {
method 'POST'
url '/beer'
body(
name: "marcin"
)
headers {
contentType(applicationJson())
}
}
response {
status 200
body(
previousStatus: "SOBER",
currentStatus: "TIPSY"
)
headers {
contentType(applicationJson())
}
}
}
Congratulations! You have successfully created your first contract!
Now we need to define the next two contracts: 2_tipsy.groovy
and 3_drunk.groovy
. You
can examine the solution. The first transition will be from
TIPSY
→ DRUNK
. The last transition will be from DRUNK
→ WASTED
.
We have managed to define all the scenarios. Now we would like to generate the stubs so that we can reuse them on the consumer side. To that end, we must set up the Spring Cloud Contract plugin in the cloned repository, as shown in the following code:
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/0.0.1-SNAPSHOT
for Maven orbuild/stubs/META-INF/com.example/beer-api-producer/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.
-
If you check out the 1_sober.json
intoxication stub, you can see the following section:
"scenarioName" : "Scenario_intoxication",
"requiredScenarioState" : "Started",
"newScenarioState" : "Step1"
In this section, WireMock is told that:
* The name of the scenario is Scenario_intoxication
. The name comes from the folder in
which the contracts were placed.
* The required scenario state is Started
. That’s the initial state.
* The next step is Step1
. Every subsequent step will be called Step
with appropriate
number appended.
If you check out 2_tipsy.json
, you can see that the required values of the previous and
next states got updated:
"scenarioName" : "Scenario_intoxication",
"requiredScenarioState" : "Step1",
"newScenarioState" : "Step2"
We have managed to install the stubs locally. it’s time to move back to our consumer
test. Let’s open the IntoxicationController
class and write the missing implementation.
You can try to write it yourself or check out the solution.
Turning on Stub Runner in HTTP Consumer Tests
After writing the implementation, if we rerun the tests, we get a connection refused exception. That happens because we have yet to start the HTTP server with the stubs, as we do in the following snippet:
-
Now it’s time to turn on the magic! Let’s add the Spring Cloud Starter Contract Stub Runner test dependency.
Maven<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-contract-stub-runner</artifactId> <scope>test</scope> </dependency>
GradletestImplementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
-
We can annotate our test class with
@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL, ids = "com.example:beer-api-producer:+:stubs:8090")
. What that will do is:-
it will download the stub JARs from Maven local (
stubsMode = StubRunnerProperties.StubsMode.LOCAL
) -
it will search for a JAR with coordinates
com.example:beer-api-producer
with latest version (+
) andstubs
classifier. Once it’s found the fake HTTP server stub will be started at port8090
-
-
Rerun the test - it should automagically pass!
-
In the logs you will see information about downloading, unpacking and starting stubs (see the logs)
-
What happened is that we could interact with real API without writing a single line of production code
-
Congratulations! You have successfully created the contracts, defined the API that suits your needs, and written the consumer part of the functionality! Now it’s time to create a pull request with the contract proposal and file it 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
-
-
Important
|
We were setting the plugin in the following since most likely you use the same
producer codebase as you have for previous tutorials. That is why we want the other
tests to still pass. If that’s not the case (if you have only just started with this
particular tutorial) then you can remove the packageWithBaseClasses entry.
|
-
The intoxication base class lays under the
intoxication
folder. Let’s use thebaseClassMappings
to set point the plugin to proper base classes, as shown in the following snippet (for both Maven and Gradle):
<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>
<baseClassMappings>
<baseClassMapping>
<contractPackageRegex>.*intoxication.*</contractPackageRegex>
<baseClassFQN>com.example.intoxication.BeerIntoxicationBase</baseClassFQN>
</baseClassMapping>
</baseClassMappings>
</configuration>
</plugin>
contracts {
testFramework = "JUNIT5"
packageWithBaseClasses = 'com.example'
baseClassMappings {
baseClassMapping(".*intoxication.*", "com.example.intoxication.BeerIntoxicationBase")
}
}
Generating Tests from Contracts
Let’s generate the tests! To do so, use the following code (for both Maven and Gradle):
$ ./mvnw clean install
+
$ ./gradlew clean build publishToMavenLocal
Consider a situation in which, suddenly, some tests start failing. Those tests are the
autogenerated tests created by Spring Cloud Contract. The tests can be found under
/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/beer
,
in the target
directory for Maven and in the build
directory for Gradle. There is a
test for each folder in which you store your contracts. The name of the test
class is the name of that folder. Each of the contracts is a single test inside that test
class. If you check out the generated IntoxicationTest
, you can seee that it got
annotated with @FixMethodOrder(MethodSorters.NAME_ASCENDING)
in order to ensure
that the tests are executed sequentially.
Now we can fix the broken tests by providing the missing implementation.
Fixing Broken HTTP Tests
Let’s start with HTTP. First, let’s write the missing implementation in
BeerServingController
. The logic to be written is simple: The
responseProvider.thereYouGo(…)
returns the Response
. The implementation is
basically a one liner, as shown in the following code snippet:
return this.responseProvider.thereYouGo(customer);
Let’s fix the BeerIntoxicationBase
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 to test that
the feature is working. That’s why we shouldn’t be accessing databases and taking similar
actions. That means that we can work with a fake instance of the ResponseProvider
.
Let’s start by writing the missing implementation of the MockResponseProvider
(Show solution). You need to:
* ensure that the name
is equal to marcin
* depending on the current
state you’ll need to set the previous
and
current
one and create the Response
.
We need to maintain state between the tests. If you try to store the state in a field in a base class, you lose it between test executions, because JUnit is reinitializing all the fields. We can fix that by setting up a small Spring Context to be reused. The following example shows what that annotation might look like:
+
@SpringBootTest(classes = BeerIntoxicationBase.Config.class)
We want RestAssured and MockMvc to reuse the web context that we have in our test. That’s why we need to set it up by using the following notation:
@Autowired WebApplicationContext webApplicationContext;
@BeforeEach
public void setup() {
RestAssuredMockMvc.webAppContextSetup(webApplicationContext);
}
Now, when try re-running the build to regenerate the tests, the tests should pass. You could now merge the pull request to master, and your CI system would build a fat jar and the stubs.
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:+: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;
Scenario contracts
package contracts.beer.intoxication
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description("""
Represents second step of getting fully drunk
given:
you were tipsy
when:
you get a beer
then:
you'll be drunk
""")
request {
method 'POST'
url '/beer'
body(
name: "marcin"
)
headers {
contentType(applicationJson())
}
}
response {
status 200
body(
previousStatus: "TIPSY",
currentStatus: "DRUNK"
)
headers {
contentType(applicationJson())
}
}
}
package contracts.beer.intoxication
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description("""
Represents last step of getting fully drunk
given:
you were drunk
when:
you get a beer
then:
you'll be wasted
""")
request {
method 'POST'
url '/beer'
body(
name: "marcin"
)
headers {
contentType(applicationJson())
}
}
response {
status 200
body(
previousStatus: "DRUNK",
currentStatus: "WASTED"
)
headers {
contentType(applicationJson())
}
}
}
Intoxication Controller
package com.example.intoxication;
import java.net.MalformedURLException;
import java.net.URI;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* @author Marcin Grzejszczak
*/
@RestController
class IntoxicationController {
private final RestTemplate restTemplate;
int port = 8090;
IntoxicationController(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@RequestMapping(method = RequestMethod.POST,
value = "/wasted",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public Response gimmeABeer(@RequestBody Person person) throws MalformedURLException {
//remove::start[]
return this.restTemplate.exchange(
RequestEntity
.post(URI.create("http://localhost:" + this.port + "/beer"))
.contentType(MediaType.APPLICATION_JSON)
.body(person),
Response.class).getBody();
//remove::end[return]
}
}
class Person {
public String name;
public Person(String name) {
this.name = name;
}
public Person() {
}
}
class Response {
public DrunkLevel previousStatus;
public DrunkLevel currentStatus;
public Response(DrunkLevel previousStatus, DrunkLevel currentStatus) {
this.previousStatus = previousStatus;
this.currentStatus = currentStatus;
}
public Response() {
}
}
enum DrunkLevel {
SOBER, TIPSY, DRUNK, WASTED
}
BeerIntoxicationBase
package com.example.intoxication;
//remove::start[]
import io.restassured.module.mockmvc.RestAssuredMockMvc;
//remove::end[]
import org.junit.jupiter.api.BeforeEach;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.context.WebApplicationContext;
import static com.example.intoxication.DrunkLevel.DRUNK;
import static com.example.intoxication.DrunkLevel.SOBER;
import static com.example.intoxication.DrunkLevel.TIPSY;
import static com.example.intoxication.DrunkLevel.WASTED;
/**
* Tests for the scenario based stub
*/
@SpringBootTest(classes = BeerIntoxicationBase.Config.class)
public abstract class BeerIntoxicationBase {
@Autowired WebApplicationContext webApplicationContext;
@BeforeEach
public void setup() {
//remove::start[]
EncoderConfig encoderConfig = new EncoderConfig().appendDefaultContentCharsetToContentTypeIfUndefined(false);
RestAssuredMockMvc.config = new RestAssuredMockMvcConfig().encoderConfig(encoderConfig);
RestAssuredMockMvc.webAppContextSetup(this.webApplicationContext);
//remove::end[]
}
@Configuration
@EnableAutoConfiguration
static class Config {
@Bean BeerServingController controller() {
return new BeerServingController(responseProvider());
}
@Bean ResponseProvider responseProvider() {
return new MockResponseProvider();
}
}
//tag::mock[]
static class MockResponseProvider implements ResponseProvider {
private DrunkLevel previous = SOBER;
private DrunkLevel current = SOBER;
@Override public Response thereYouGo(Customer personToCheck) {
//remove::start[]
if ("marcin".equals(personToCheck.name)) {
switch (this.current) {
case SOBER:
this.current = TIPSY;
this.previous = SOBER;
break;
case TIPSY:
this.current = DRUNK;
this.previous = TIPSY;
break;
case DRUNK:
this.current = WASTED;
this.previous = DRUNK;
break;
case WASTED:
throw new UnsupportedOperationException("You can't handle it");
}
}
//remove::end[]
return new Response(this.previous, this.current);
}
}
//end::mock[]
}