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

scenario
Figure 1. Stateful scenario. The more you drink the more wasted you get

Flow

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

consumer flow 1
Figure 3. Interact with cloned producer code

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 SOBERTIPSYDRUNKWASTED.

  • 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 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 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 TIPSYDRUNK. The last transition will be from DRUNKWASTED.

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>
    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/0.0.1-SNAPSHOT for Maven or build/stubs/META-INF/com.example/beer-api-producer/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.

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>
    Gradle
    testImplementation("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 (+) and stubs classifier. Once it’s found the fake HTTP server stub will be started at port 8090

  • 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

producer flow 1
Figure 4. 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 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>
        Gradle
        contracts {
        	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 the baseClassMappings to set point the plugin to proper base classes, as shown in the following snippet (for both Maven and Gradle):

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>
        <baseClassMappings>
            <baseClassMapping>
                <contractPackageRegex>.*intoxication.*</contractPackageRegex>
                <baseClassFQN>com.example.intoxication.BeerIntoxicationBase</baseClassFQN>
            </baseClassMapping>
        </baseClassMappings>
    </configuration>
</plugin>
Gradle
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):

Maven
$ ./mvnw clean install

+

Gradle
$ ./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

consumer flow 2
Figure 5. 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:+: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;

Scenario contracts

Tipsy
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())
		}
	}
}
Drunk
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[]
}

Back to the Main Page