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:

scenario stubs per consumer 1
Figure 1. Unofficially addresses the person by name for positive beer selling via HTTP

   

scenario stubs per consumer 2
Figure 2. Unofficially addresses the person by name for negative beer selling via HTTP

   

scenario stubs per consumer 3
Figure 3. Officially addresses the person by surname for positive beer selling via HTTP

   

scenario stubs per consumer 4
Figure 4. Officially addresses the person by surname for negative beer selling via HTTP

   

Flow

flow
Figure 5. Consumer Driven Contract 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

consumer flow 1
Figure 6. Interact with cloned producer code

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 and age, we ask the producer to verify whether the person is eligible to get the beer.

      • We expect to retrieve the status and the name 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’s name.

      • If the person (for example, with the name, marcin) can get the beer then we say THERE 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 and age, we ask the producer to verify whether the person is eligible to get the beer.

      • We expect to retrieve the status and the surname of the client (for simplicity we receive the provided name as surname).

      • 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 say THERE YOU GO MR [marcin], Where marcin came back from the producer service as a surname.

      • 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 a Person 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 dependency

    Maven
    <dependency>
    	<groupId>org.springframework.cloud</groupId>
    	<artifactId>spring-cloud-starter-contract-verifier</artifactId>
    	<scope>test</scope>
    </dependency>
    Gradle
    testImplementation("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:

  1. Under the rest folder, create a file called shouldGrantABeerIfOldEnough.groovy

  2. 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"]. The body method accepts a map. In Groovy, if a method accepts a map, then the [] brackets can be omitted. So you can write either body([age:22, name:"marcin"]) or body(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 $() or value() 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 return OK when the person can get the beer. The foo-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 the name 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.

  1. Set the age in the request to the following regular expression [0-1][0-9].

  2. Update the response body to return a status of NOT_OK.

  3. 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>
    Gradle
    buildscript {
    	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’s verifierVersion 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 or build/stubs/META-INF/com.example/beer-api-producer-with-stubs-per-consumer/0.0.1-SNAPSHOT for Gradle. Over there you’ll see contracts folder where all contracts got copied and the mappings folder where you’ll find all the generated stubs. By default Spring Cloud Contract uses WireMock as an implementation of fake HTTP server. Under the rest 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 or NOT_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:

+

Maven
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
	<scope>test</scope>
</dependency>

+

Gradle
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

producer flow 1
Figure 7. Producer takes over the PR, writes missing impl and merges the PR

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 contract src/test/resources/contracts/foo/bar/shouldDoSth.groovy would create a test class called BarTest that would extend FooBarBase 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:

Maven
<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>
Gradle
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 in target for Maven or build 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 the response 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 the shouldGetBeer method in such a way that if the user is old enough then the method will return true. Let’s now add the oldEnoughMethod()

      	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 for PersonToCheck. In this case if the person to check is older or is 20 then the method shouldGetBeer method will return true.

    • 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

consumer flow 2
Figure 8. Switch to work online
  • 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 the stubsMode to StubRunnerProperties.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 an application.yml and stubrunner.stubs-mode equal to remote

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

Maven
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-contract-verifier</artifactId>
	<scope>test</scope>
</dependency>
Gradle
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier")

Proposal of simple contracts by consumer

HTTP communication

Old Enough
// 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())
		}
	}
}
Too Young
// 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

Positive Verification
// 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())
		}
	}
}
Negative Verification
// 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

Positive Verification
// 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())
		}
	}
}
Negative Verification
// 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

Successful
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())
        }
    }
}
Unsuccessful
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

Successful
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())
        }
    }
}
Unsuccessful
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);

Back to the Main Page