In this tutorial, we keep the contracts together with the producer code, and we check out the more advanced concepts behind Spring Cloud Contract.

Scenarios

We’ll try to code the following scenario:

grumpy 1
Figure 1. Grumpy bartender that doesn’t want to sell any alcohol

   

grumpy 2
Figure 2. But the beer will always be sold to Josh Long

   

Flow

flow
Figure 3. 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 4. Interact with cloned producer code
Important
This tutorial assumes that you completed the previous tutorials and that the consumer code has already been set up with appropriate dependencies.

Let’s open the GrumpyBartenderControllerTest. The first step is to write the missing implementation of the tests. Basing on the previously shown requirements, the controller should resemble the following:

	@Test public void should_fail_to_sell_beer() throws Exception {
		//remove::start[]
		this.mockMvc.perform(MockMvcRequestBuilders.post("/grumpy")
				.contentType(MediaType.APPLICATION_JSON)
				.content(this.json.write(new Person("marcin", 22)).getJson()))
				.andExpect(status().isOk())
				.andExpect(jsonPath("$.whatTheBartenderSaid").value("You're drunk [marcin]. Go home!"))
				.andExpect(jsonPath("$.whatDoWeDo").value("Go to another bar"));
		//remove::end[]
	}

	@Test public void should_sell_beer_to_Josh() throws Exception {
		//remove::start[]
		this.mockMvc.perform(MockMvcRequestBuilders.post("/grumpy")
				.contentType(MediaType.APPLICATION_JSON)
				.content(this.json.write(new Person("starbuxman", 22)).getJson()))
				.andExpect(status().isOk())
				.andExpect(jsonPath("$.whatTheBartenderSaid").value("There you go Josh!"))
				.andExpect(jsonPath("$.whatDoWeDo").value("Enjoy!"));
		//remove::end[]
	}

If we run the tests now, they fail. Let’s now check out the producer’s code to define the missing contract. Open the producer_advanced project.

Cloned Producer

As usual in these tutorials, we do not clone the producer’s code, even though we would do so in a real life scenario.

Now let’s write our contract! You can define the contracts with Groovy DSL. Let’s create our first HTTP contract. To do so:

  1. Under src/test/resources/contracts/beer/rest/, create a shouldNotSellAnyAlcohol.groovy file.

  2. Call the Contract.make method to start defining the contract, as follows:

org.springframework.cloud.contract.spec.Contract.make {

}
  • You can call the description() method to provide some meaningful description.

Tip
You can use the Groovy multiline String """ """ to have all special characters escaped. Every new line in the String is converted into a new line character. The following code shows an example:
org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
}

Now call the request { } and response { } methods, as shown below:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
    }
    response {
    }
}

Let’s assume that we want to send a POST method. Call method POST() or method "POST".

Tip
In Groovy, you don’t need to provide parentheses (in most cases). You can write either method POST() or method(POST()). In both cases, the effet is the same.
org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
    }
    response {
    }
}

Now we need to provide a URL. You can set it to /buy by writing url "/buy", as shown in the following code:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/buy"
    }
    response {
    }
}

Now it’s time to define the body. We want to define age and name fields. Let’s make name accept any alpha unicode value. Let’s make age be a concrete value at this time. We will make it dynamic in the next step. The following code sets our initial values for both fields:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
    }
    response {
    }
}

As you can see, we used the $() method where we’ve used the anyAlphaUnicode() function to set a dynamic value.

Now we can make the age field be dynamic. In Spring Cloud Contract, you can either provide the dynamic values directly in the body (as we have with the name) or via stubMatchers and testMatchers sections. Let’s use the first one. We define the JSON path for which we want to define a dynamic value, as follows:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
    }
    stubMatchers {
        jsonPath('$.age', byRegex('[2-9][0-9]'))
    }
    response {
    }
}

Inside the stubMatchers section, we defined that an element matching a JSON path of $.age has to match a regular expression of [2-9][0-9]. You can see that there are methods other than byRegex. You can read about them in the documentation.

Important
If you provide a value via the matchers section, then the value for the key for which you added the matching is removed from automatic test assertion generation. You have to provide those values manually via the matchers section.

Now we can create the headers by calling the headers { } method, as shown in the following code:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {

        }
    }
    response {
    }
}

Inside that method, let’s define that we want to use the Content-Type: "application/json" header. Just call contentType(applicationJson()) methods:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
    }
}

Congratulations! You defined how you would like the contract for the request to look!

Now we can work on the response. In the response block, we want to define that the status of our response will be 200. To do so, call status 200, as shown in the following code:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
        status 200
    }
}

We want our response to have a body. We want to use the message and status fields. In the message, we want to respond with You’re drunk [name from request]. Go home! For the status, we want to always return a NOT_OK value and have a custom assertion in our tests.

Let’s start with the message field. Spring Cloud Contract gives you a method, called fromRequest(), that lets you specify in the response that you would like to fetch some values from the request. In our case, the value we want in the request is inside the request body under the $.name JSON path. Consequently, we can set the value of message to "You’re drunk [${fromRequest().body('$.name')}]. Go home!",. Note that we have a "" Groovy String with a ${} String interpolation in which we’re calling the fromRequest() method. The following code shows how all of that works:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
        status 200
        body(
            message: "You're drunk [${fromRequest().body('$.name')}]. Go home!"
        )
    }
}

Now we have the message response that references the request. It’s time for the second field. Until now, we always provided a single value for the dynamic parts of the contract. Whenever we have dynamic values on one side (consumer or producer), then we must have a concrete value on the other side. In this case, we can provide that value manually. For the response in the stub, we need to provide a concrete value equal to NOT_OK. For the generated test, we want to have a custom assertion done via the assertStatus() method defined in the base class. To achieve that, we need to write $(c("NOT_OK"), p(execute('assertStatus($it)'))). Let’s now analyze this syntax:

  • c() is a shortcut for consumer(), and p() is short for producer(). By calling `$(c(),p()), we provide a concrete value for the consumer and a dynamic one for the producer.

  • The $(c("NOT_OK"),…​) means that, for the response in the stub, for the status field, we want the stub to contain a value of NOT_OK.

  • The $(…​,p(execute('assertStatus($it)'))) means that we want, on the producer side, in the autogenerated tests, to run a method defined in the base class. That method is called assertStatus(). As a parameter of that method, we want to pass the value of an element present in the response JSON. In our case, we provide a dynamic value for the $.status field. assertStatus($it) gets translated to assertStatus(read the $.status from the response JSON)

Now we can write the response body, as shown in the following code:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
        status 200
        body(
            message: "You're drunk [${fromRequest().body('$.name')}]. Go home!",
            status: $(c("NOT_OK"), p(execute('assertStatus($it)')))
        )
    }
}

Now we may want to perform some more complex analysis of the message field through a method called assertMessage(). There’s another way to do that: We can call the testMatchers section.

Under testMatchers, we can define, through a JSON path, the element we want to dynamically assert. To do so, we can add the following section:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
        status 200
        body(
            message: "You're drunk [${fromRequest().body('$.name')}]. Go home!",
            status: $(c("NOT_OK"), p(execute('assertStatus($it)')))
        )
        testMatchers {
            jsonPath('$.message', byCommand('assertMessage($it)'))
        }
    }
}

The last thing to add are the response headers. We do exactly the same thing as we previously did for the request, except that we use headers { contentType(applicationJson()) }, as shown in the following example:

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
        status 200
        body(
            message: "You're drunk [${fromRequest().body('$.name')}]. Go home!",
            status: $(c("NOT_OK"), p(execute('assertStatus($it)')))
        )
        testMatchers {
            jsonPath('$.message', byCommand('assertMessage($it)'))
        }
        headers {
            contentType(applicationJson())
        }
    }
}

We’re almost done. We wrote a very generic example that catches a person with any name. However, in the requirements, we saw that a starbuxman person always has to get the beer. Consequently, we need a specific case of our generic pattern. That is why we will set the priority to 100 (the higher the number, the lower the priority).

org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: $(anyAlphaUnicode()),
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
        status 200
        body(
            message: "You're drunk [${fromRequest().body('$.name')}]. Go home!",
            status: $(c("NOT_OK"), p(execute('assertStatus($it)')))
        )
        testMatchers {
            jsonPath('$.message', byCommand('assertMessage($it)'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    priority 100
}

Congratulations! You have created your first contract! Now we can define a version for someone named Josh.

  1. Copy the file and call it shouldSellAlcoholOnlyToStarbuxman.groovy.

  2. Set the name to starbuxman.

  3. Set the priority to 10.

  4. Set the message to There you go Josh!.

  5. Set the status to OK.

Note
We do not want to do any custom server-side assertions.
org.springframework.cloud.contract.spec.Contract.make {
    description("""
        Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
    """)
    request {
        method POST()
        url "/check"
        body(
            name: "starbuxman",
            age: 25
            )
        }
        stubMatchers {
            jsonPath('$.age', byRegex('[2-9][0-9]'))
        }
        headers {
            contentType(applicationJson())
        }
    }
    response {
        status 200
        body(
                message: "There you go Josh!",
                status: "OK"
        )
        headers {
            contentType(applicationJson())
        }
    }
    priority 10
}

Congratulations! You have created all the contracts. Now we can go ahead and install the stubs, as shown in the following code for both Maven and Gradle:

Maven
./mvnw clean install -DskipTests
Gradle
./gradlew clean build publishToMavenLocal

Now we can go back to our consumer tests and open the GrumpyBartenderControllerTest class. We want to add Stub Runner as usual. However, this time, we do not pass the port. We want the port to be set automatically.

@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL, ids = "com.example:beer-api-producer-advanced")

How can you retrieve the port value then? There are two ways of doing so:

  • Autowire the field annotated with @Value("${stubrunner.runningstubs.artifactid.port}").

  • Use the autowired StubFinder interface, which includes the findStub method.

The following code shows an example of autowiring the field for beer-api-producer-advanced:

@Value("${stubrunner.runningstubs.beer-api-producer-advanced.port}") int stubPort;

@BeforeEach
public void setupPort() {
    controller.port = stubPort;
}

The following code shows an example of using the StubFinder interface for beer-api-producer-advanced:

@Autowired StubFinder stubFinder;

@BeforeEach
public void setupPort() {
    controller.port = stubFinder.findStubUrl("beer-api-producer-advanced").getPort();
}

Let’s go with the first option (autowiring the field). We want to inject the value of the port of the running stub into the controller. Now, when we run the tests, then they should pass successfully.

Congratulations! As consumers, we successfully used the API of the producer for both HTTP and messaging. Now we can file a pull request (PR) to the producer’s code with the proposal of the contract. Let’s switch to the producer side.

Producer Flow 1

producer flow 1
Figure 5. 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

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

The situation is that the generated tests have failed. We need to fix it by providing the missing assert methods in the base class. We also need to set up RestAssured. Let’s start with the first one:

  1. Go to BeerRestBase.

  2. Add the missing assertStatus(String status) and assertMessage(String message) methods (or copy from the solution).

    • assertStatus should assert that the status is equal to NOT_OK.

    • assertMessage should assert that the message contains Go home!

  3. Add the missing Rest Assured setup by adding the BuyController into the list of standalone set up classes, as shown in the following code snippet:


		// https://github.com/spring-cloud/spring-cloud-contract/issues/1428
		EncoderConfig encoderConfig = new EncoderConfig().appendDefaultContentCharsetToContentTypeIfUndefined(false);
		RestAssuredMockMvc.config = new RestAssuredMockMvcConfig().encoderConfig(encoderConfig);
		RestAssuredMockMvc.standaloneSetup(..., new BuyController(), ...);

This time, we are not mocking in the base class, because we already did so in the previous tutorials. Instead, we try to make our tests pass as fast as possible.

Important
We want our Controller to use the async servlet functionality. That’s why we need it to return a Callable<Response>

To ensure that we return a Callable<Response>, we need to write our implementation of the controller (or copy from the solution) such that it has the following characteristics:

  • A POST method to the /buy endpoint that produces and consumes JSON

  • If the Person name is equal to starbuxman, return a status code of OK and a message of There you go Josh.

  • If the Person name is not equal to starbuxman, return a status code of NOT_OK and a message of You’re drunk [name]. Go home!

Now, when you run the build again, your autogenerated tests still fail. The reason for the failure is that we used the async servlet feature but Rest Assured does not know that. To fix that, we need to add the async() method in the response side of our contracts. (Check the solution). Now, when you run the build again, your tests should pass.

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;

BuyController

	@PostMapping(value = "/buy",
			consumes = MediaType.APPLICATION_JSON_VALUE,
			produces = MediaType.APPLICATION_JSON_VALUE)
	Callable<Response> buy(@RequestBody Person person) {
		if ("starbuxman".equalsIgnoreCase(person.name)) {
			return () -> new Response(Status.OK, "There you go Josh!");
		}
		return () ->new Response(Status.NOT_OK, "You're drunk [" + person.name + "]. Go home!");
	}

Missing assert methods

	protected void assertStatus(String status) {
		BDDAssertions.then(status).isEqualToIgnoringCase(Status.NOT_OK.name());
	}

	protected void assertMessage(String message) {
		BDDAssertions.then(message).contains("Go home!");
	}

Grumpy contracts

Everbody
// rest/shouldNotSellAnyAlcohol.groovy
package contracts.beer.rest

import org.springframework.cloud.contract.spec.Contract

Contract.make {
	description("""
Represents a grumpy waiter that is too bored to sell any alcohol for anyone.
""")
	request {
		method POST()
		url '/buy'
		body(
				name: $(anyAlphaUnicode()),
				age: 25
		)
		bodyMatchers {
			jsonPath('$.age', byRegex('[2-9][0-9]'))
		}
		headers {
			contentType(applicationJson())
		}
	}
	response {
		status 200
		body(
				message: "You're drunk [${fromRequest().body('$.name')}]. Go home!",
				status: $(c("NOT_OK"), p(execute('assertStatus($it)')))
		)
		bodyMatchers {
			jsonPath('$.message', byCommand('assertMessage($it)'))
		}
		headers {
			contentType(applicationJson())
		}
		async()
	}
	priority 100
}
Starbuxman
// rest/shouldSellAlcoholToStarbuxman.groovy
package contracts.beer.rest

import org.springframework.cloud.contract.spec.Contract

Contract.make {
	description("""
Represents a grumpy waiter that will sell alcohol only to Starbuxman.
""")
	request {
		method POST()
		url '/buy'
		body(
				name: "starbuxman",
				age: 25
		)
		bodyMatchers {
			jsonPath('$.age', byRegex('[2-9][0-9]'))
		}
		headers {
			contentType(applicationJson())
		}
	}
	response {
		status 200
		body(
				message: "There you go Josh!",
				status: "OK"
		)
		headers {
			contentType(applicationJson())
		}
		async()
	}
	priority 10
}

Back to the Main Page