Spring Cloud Contract 1.0.0.M1 Released

Engineering | Marcin Grzejszczak | July 25, 2016 | ...

On behalf of the Spring Cloud team it is my pleasure to announce the 1.0.0.M1 release of the new Spring Cloud project called Spring Cloud Contract. You can grab it from the Spring’s milestone repository or even better - go to start.spring.io and pick it from there.

Spring Cloud Contract

The microservice approach has plenty of benefits but also introduces complexity. This is an inevitable result of working with distributed systems: with increasing complexity inevitably more questions are posed. In this article we show how to test microservices and create a better API by using the Consumer Driven Contracts approach. In order to make testing microservices easier we are more than happy to introduce a new project in the family of Spring Cloud projects - Spring Cloud Contract. This project provides support for Consumer Driven Contracts and service schemas in Spring applications, covering a range of options for writing tests, publishing them as assets, asserting that a contract is kept by producers and consumers, for HTTP and message-based interactions.

This article is a companion of another recent one on how to do zero-downtime deployment with a database.

Service providers and consumers

Before we jump into the details let’s go though some theory. One of the biggest challenges related to the distributed systems is the agreement on the structure of messages that pass between nodes (by "message" we mean any well-formed, non-streaming data, so that applies to traditional HTTP APIs and also event-based microservices). Here are a few questions that arise when we think about message structure:

  • How can the consumer know that the producer has changed its API?

  • How can the producer side know if it’s going to break the consumers?

  • If the consumer is using stubs for testing, who should create those stubs?

  • How can you ensure the quality of stubs?

One of the ways to solve these problems is to introduce the notion of a contract. A contract is an agreement between a provider and a consumer in terms of what their communication should look like. The questions remains on who should drive the change of the API, where the contract should be stored and what the contract should contain.

In this blog post we’ll present the approach called "Consumer Driven Contracts" together with the new Spring Cloud project called Spring Cloud Contract Verifier formerly known as Accurest and hosted by the Codearte company.

Meanwhile in a company…​

Let’s imagine a following scenario:

The producer side team finishes its sprint and changes their application by introducing a new version of the API. Since there is not much time due to tight schedules there were no consultations with the consumers of that API. The consumers had all the producer side integrations mocked in their tests and, since nobody has informed them about any changes, they didn’t update those mocks. That’s why all the unit and integration tests were still green but end to end tests failed miserably. When the consumers noticed that the producer side API has changed they had to invest a lot of time to adjust their production and test code in order to send and receive the new, required data. The consumer team decided that it’s close to impossible (due to tight schedule and complexity of the changes) so they started filing issues to the producer side team to adjust their API. The producer side team replies that "there is no time" and that they can "talk to their product owner so he puts that requirement into their backlog".

If you took a look at the retrospectives of both teams you could see the following.

For the consumer side:

  • a breaking change was introduced and nobody informed us

  • the producer side team hasn’t consulted on their API changes - the new API is unusable

  • our integration tests didn’t catch the change of the producer side API

  • we got completely ignored by the producer side team in terms of adjusting their API

For the producer side:

  • everybody is angry with us but we have to deliver business value

  • we can not update every single consumer team’s tests when we change our API

Rings a bell? Don’t worry, there are ways to change this approach to make everybody less annoyed.

What is Consumer Driven Contracts?

There a few problems presented in the aforementioned scenario:

  • the API change was not made in consultation with the consumers

  • the stubbing process is owned by the consumers thus no changes of the producer side are reflected

Let’s focus on the first problem. Do you remember the Test Driven Development (TDD) approach? You start with an expectation in a form of a test, then you write an implementation to make the test pass and finally you refactor to make the code look nicer. "Red, green, refactor" - failing test, passing test, refactored code. TDD is about making mistakes. Mistakes related to the assumption how your code API should look like. It’s an iterative process that allows you to improve the quality of your code. The developer is the driver of the change of the code’s API. He is its user, he knows what he wants to achieve so he alters the API until he is happy with the results. Now, let’s imagine that we move this approach to the level of API design…​

Since the consumers are those who use the API they should be the drivers of the API change. The main difference between that and TDD is that here you have 2 teams taking part in the process - the consumer and the prodcuer. This is where you can profit from the Consumer Driven Contract (CDC) approach. A couple of its characteristics are:

  • the producer API is designed by the consumers together with the producer team (communication is crucial!)

  • the contracts are written down and have to suit both parties

  • the work can be decoupled - once the contracts have been noted down both teams can work independently

  • the producer side owns the contracts (this is a strictly defined responsibility of concrete people)

The Spring Cloud team wanted to have a tool that would allow us to:

  • define contracts in a readable but also a flexible manner

  • make the contracts show some use-cases and not only present structure of the messages

  • generate tests to automatically verify the contracts against the producer side

  • automatically produce stubs from the contracts so that the consumers can reuse it

  • make this approach possible for HTTP and messaging based communication

Spring Cloud Contract Verifier solves this problem by providing automated solutions to ensure the quality and reliability of the created contracts and their stubs. It consists of the following main features:

  • the core part of the library gives you the concept of a Contract

  • the Verifier is used by producers (usually via the build plugins)

  • Spring Cloud Contract Verifier Maven / Gradle plugins give you the functionality of converting the Contract into tests and WireMock stubs (WireMock is a HTTP server stub)

  • Spring Cloud Contract Stub Runner allows consumers to automatically download stubs of upstream producers and start in memory HTTP stubbed servers in your integration tests

  • Spring Cloud Contract Stub Runner also allows consumers to send and receive messages (via Spring Integration, Spring Cloud Stream or Apache Camel) described in the contracts

Let’s look at the following step-by-step example how to use the tool in case of the HTTP communication.

CDC with Spring Cloud Contract Verifier

Let’s take an example of Fraud Detection and Loan Issuance process. The business scenario is such that we want to issue loans to people but don’t want them to steal the money from us. The current implementation of our system grants loans to everybody.

Let’s assume that the Loan Issuance is a client to the Fraud Detection server. In the current sprint we are required to develop a new feature - if a client wants to borrow too much money then we mark him as fraud.

Technical remark - Fraud Detection will have artifact id http-server, Loan Issuance http-client and both have group id com.example.

Social remark - both consumer and producer development teams need to communicate directly and discuss changes while going through the process. CDC is all about communication.

The producer code is available here and consumer code here.

Consumer side (Loan Issuance)

As a developer of the Loan Issuance service (a consumer of the Fraud Detection server):

start doing TDD by writing a test to your feature

@Test
public void shouldBeRejectedDueToAbnormalLoanAmount() {
	// given:
	LoanApplication application = new LoanApplication(new Client("1234567890"),
			99999);
	// when:
	LoanApplicationResult loanApplication = sut.loanApplication(application);
	// then:
	assertThat(loanApplication.getLoanApplicationStatus())
			.isEqualTo(LoanApplicationStatus.LOAN_APPLICATION_REJECTED);
	assertThat(loanApplication.getRejectionReason()).isEqualTo("Amount too high");
}

We’ve just written a test of our new feature. If a loan application for a big amount is received we should reject that loan application with some description.

write the missing implementation

At some point in time you need to send a request to the Fraud Detection service. Let’s assume that we’d like to send the request containing the id of the client and the amount he wants to borrow from us. We’d like to send it to the /fraudcheck url via the PUT method.

ResponseEntity<FraudServiceResponse> response =
		restTemplate.exchange("http://localhost:" + port + "/fraudcheck", HttpMethod.PUT,
				new HttpEntity<>(request, httpHeaders),
				FraudServiceResponse.class);

For simplicity we’ve hardcoded the port of the Fraud Detection service at 8080 and our application is running on 8090.

If we’d start the written test it would obviously break since we have no service running on port 8080.

clone the Fraud Detection service repository locally

We’ll start playing around with the producer side. That’s why we need to first clone it.

git clone https://your-git-server.com/server.git local-http-server-repo

define the contract locally in the repo of Fraud Detection service

As consumers we need to define what exactly we want to achieve. We need to formulate our expectations. That’s why we write the following contract.

package contracts

org.springframework.cloud.contract.spec.Contract.make {
			request { // (1)
				method 'PUT' // (2)
				url '/fraudcheck' // (3)
				body([ // (4)
					clientId: value(consumer(regex('[0-9]{10}'))),
					loanAmount: 99999
					])
				headers { // (5)
					header('Content-Type', 'application/vnd.fraud.v1+json')
				}
			}
			response { // (6)
				status 200 // (7)
				body([ // (8)
					fraudCheckStatus: "FRAUD",
					rejectionReason: "Amount too high"
				])
				headers { // (9)
					 header('Content-Type': value(
							 producer(regex('application/vnd.fraud.v1.json.*')),
							 consumer('application/vnd.fraud.v1+json'))
					 )
				}
			}
}

/*
Since we don't want to force on the user to hardcode values of fields that are dynamic
(timestamps, database ids etc.), one can provide parametrize those entries by using the
`value(consumer(...), producer(...))` method. That way what's present in the `consumer`
section will end up in the produced stub. What's there in the `producer` will end up in the
autogenerated test. If you provide only the regular expression side without the concrete
value then Spring Cloud Contract will generate one for you.

From the Consumer perspective, when shooting a request in the integration test:

(1) - If the consumer sends a request
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
 * has a field `clientId` that matches a regular expression `[0-9]{10}`
 * has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/vnd.fraud.v1+json`
(6) - then the response will be sent with
(7) - status equal `200`
(8) - and JSON body equal to
 { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` equal to `application/vnd.fraud.v1+json`

From the Producer perspective, in the autogenerated producer-side test:

(1) - A request will be sent to the producer
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
 * has a field `clientId` that will have a generated value that matches a regular expression `[0-9]{10}`
 * has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/vnd.fraud.v1+json`
(6) - then the test will assert if the response has been sent with
(7) - status equal `200`
(8) - and JSON body equal to
 { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` matching `application/vnd.fraud.v1+json.*`
 */

The Contract is written using a statically typed Groovy DSL. You might be wondering what are those value(consumer(…​), producer(…​)) parts. By using this notation Spring Cloud Contract allows you to define parts of a JSON / URL / etc. which are dynamic. In case of an identifier or a timestamp you don’t want to hardcode a value. You want to allow some different ranges of values. That’s why for the consumer side you can set regular expressions matching those values. You can provide the body either by means of a map notation or String with interpolations. Consult the docs for more information. We highly recommend using the map notation!

The aforementioned contract is an agreement between two sides that:

  • if an HTTP request is sent with

    • a method PUT on an endpoint /fraudcheck

    • JSON body with clientId matching the regular expression [0-9]{10} and loanAmount equal to 99999

    • and with a header Content-Type equal to application/vnd.fraud.v1+json

  • then an HTTP response would be sent to the consumer that

    • has status 200

    • contains JSON body with the fraudCheckStatus field containing a value FRAUD and the rejectionReason field having value Amount too high

    • and a Content-Type header with a value of application/vnd.fraud.v1+json

Once we’re ready to check the API in practice in the integration tests we need to just install the stubs locally

add the Spring Cloud Contract Verifier plugin to the server side

We can add either Maven or Gradle plugin - in this example we’ll show how to add Maven. First we need to add the Spring Cloud Contract BOM.

<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-contract-dependencies</artifactId>
			<version>${spring-cloud-contract.version}</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>

Next, the Spring Cloud Contract Verifier Maven plugin

<plugin>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
	<version>${spring-cloud-contract.version}</version>
	<extensions>true</extensions>
	<configuration>
		<baseClassForTests>com.example.fraud.MvcTest</baseClassForTests>
	</configuration>
</plugin>

Since the plugin was added we get the Spring Cloud Contract Verifier features which from the provided contracts:

  • generate and run tests

  • produce and install stubs

We don’t want to generate tests since we, as consumers, want only to play with the stubs. That’s why we need to skip the tests generation and execution. When we execute:

cd local-http-server-repo
./mvnw clean install -DskipTests

In the logs we’ll see something like this:

[INFO] --- spring-cloud-contract-maven-plugin:1.0.0.BUILD-SNAPSHOT:generateStubs (default-generateStubs) @ http-server ---
[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar
[INFO]
[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ http-server ---
[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:1.4.0.BUILD-SNAPSHOT:repackage (default) @ http-server ---
[INFO]
[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ http-server ---
[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.jar
[INFO] Installing /some/path/http-server/pom.xml to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.pom
[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar

This line is extremely important

[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar

It’s confirming that the stubs of the http-server have been installed in the local repository.

run the integration tests

In order to profit from the Spring Cloud Contract Stub Runner functionality of automatic stub downloading you have to do the following in our consumer side project (Loan Application service).

Add the Spring Cloud Contract BOM

<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-contract-dependencies</artifactId>
			<version>${spring-cloud-contract.version}</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>

Add the dependency to Spring Cloud Contract Stub Runner

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

Provide the group id and artifact id for the Stub Runner to download stubs of your collaborators. Also provide the offline work switch since you’re playing with the collaborators offline (optional step).

stubrunner:
  work-offline: true
  stubs.ids: 'com.example:http-server:+:stubs:8080'

Annotate your test class with @AutoConfigureStubRunner

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureStubRunner
public class LoanApplicationServiceTests {

Now if you run your tests you’ll see sth like this:

2016-07-19 14:22:25.403  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Desired version is + - will try to resolve the latest version
2016-07-19 14:22:25.438  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved version is 0.0.1-SNAPSHOT
2016-07-19 14:22:25.439  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolving artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT using remote repositories []
2016-07-19 14:22:25.451  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar
2016-07-19 14:22:25.465  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacking stub from JAR [URI: file:/path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar]
2016-07-19 14:22:25.475  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacked file to [/var/folders/0p/xwq47sq106x1_g3dtv6qfm940000gq/T/contracts100276532569594265]
2016-07-19 14:22:27.737  INFO 41050 --- [           main] o.s.c.c.stubrunner.StubRunnerExecutor    : All stubs are now running RunningStubs [namesAndPorts={com.example:http-server:0.0.1-SNAPSHOT:stubs=8080}]

Which means that Stub Runner has found your stubs and started a server for app with group id com.example, artifact id http-server with version 0.0.1-SNAPSHOT of the stubs and with stubs classifier on port 8080.

file a PR

What we did until now is an iterative process. We can play around with the contract, install it locally and work on the consumer side until we’re happy with the contract.

Once we’re satisfied with the results and the test passes publish a PR to the producer side. At this point the consumer side work is done.

Producer side (Fraud Detection server)

As a developer of the Fraud Detection server (a producer of messages consumed by the Loan Issuance service):

initial implementation

As a reminder here you can see the initial implementation

@RequestMapping(
		value = "/fraudcheck",
		method = PUT,
		consumes = FRAUD_SERVICE_JSON_VERSION_1,
		produces = FRAUD_SERVICE_JSON_VERSION_1)
public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);

take over the PR

git checkout -b contract-change-pr master
git pull https://your-git-server.com/server-side-fork.git contract-change-pr

You have to add the dependencies needed by the autogenerated tests

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

In the configuration of the Maven plugin we passed the baseClassForTests property

<plugin>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
	<version>${spring-cloud-contract.version}</version>
	<extensions>true</extensions>
	<configuration>
		<baseClassForTests>com.example.fraud.MvcTest</baseClassForTests>
	</configuration>
</plugin>

That’s because all the generated tests will extend that class. Over there you can set up your Spring Context or whatever is necessary. In our case we’re using Rest Assured MVC to start the producer side FraudDetectionController.

package com.example.fraud;

import com.example.fraud.FraudDetectionController;
import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc;

import org.junit.Before;

public class MvcTest {

	@Before
	public void setup() {
		RestAssuredMockMvc.standaloneSetup(new FraudDetectionController());
	}

	public void assertThatRejectionReasonIsNull(Object rejectionReason) {
		assert rejectionReason == null;
	}
}

Now, if you run the ./mvnw clean install you would get sth like this:

Results :

Tests in error:
  ContractVerifierTest.validate_shouldMarkClientAsFraud:32 » IllegalState Parsed...

That’s because you have a new contract from which a test was generated and it failed since you haven’t implemented the feature. The autogenerated test would look like this:

@Test
public void validate_shouldMarkClientAsFraud() throws Exception {
    // given:
        MockMvcRequestSpecification request = given()
                .header("Content-Type", "application/vnd.fraud.v1+json")
                .body("{\"clientId\":\"1234567890\",\"loanAmount\":99999}");

    // when:
        ResponseOptions response = given().spec(request)
                .put("/fraudcheck");

    // then:
        assertThat(response.statusCode()).isEqualTo(200);
        assertThat(response.header("Content-Type")).matches("application/vnd.fraud.v1.json.*");
    // and:
        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
        assertThatJson(parsedJson).field("fraudCheckStatus").matches("[A-Z]{5}");
        assertThatJson(parsedJson).field("rejectionReason").isEqualTo("Amount too high");
}

As you can see all the producer() parts of the Contract that were present in the value(consumer(…​), producer(…​)) blocks got injected into the test.

What’s important here to note is that on the producer side we also are doing TDD. We have expectations in form of a test. This test is shooting a request to our own application to an URL, headers and body defined in the contract. It also is expecting very precisely defined values in the response. In other words you have is your red part of red, green and refactor. Time to convert the red into the green.

write the missing implementation

Now since we now what is the expected input and expected output let’s write the missing implementation.

@RequestMapping(
		value = "/fraudcheck",
		method = PUT,
		consumes = FRAUD_SERVICE_JSON_VERSION_1,
		produces = FRAUD_SERVICE_JSON_VERSION_1)
public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
if (amountGreaterThanThreshold(fraudCheck)) {
	return new FraudCheckResult(FraudCheckStatus.FRAUD, AMOUNT_TOO_HIGH);
}
return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);
}

If we execute ./mvnw clean install again the tests will pass. Since the Spring Cloud Contract Verifier plugin adds the tests to the generated-test-sources you can actually run those tests from your IDE.

deploy your app

Once you’ve finished your work it’s time to deploy your change. First merge the branch

git checkout master
git merge --no-ff contract-change-pr
git push origin master

Then we assume that your CI would run sth like ./mvnw clean deploy which would publish both the application and the stub artifcats.

Consumer side (Loan Issuance) final step

As a developer of the Loan Issuance service (a consumer of the Fraud Detection service):

merge branch to master

git checkout master
git merge --no-ff contract-change-pr

work online

Now you can disable the offline work for Spring Cloud Contract Stub Runner ad provide where the repository with your stubs is placed. At this moment the stubs of the producer side will be automatically downloaded from Nexus / Artifactory.

stubrunner.stubs:
  ids: 'com.example:http-server:+:stubs:8080'
  repositoryRoot: http://repo.spring.io/libs-snapshot

And that’s it!

Summary

In this example you could see how to use the Spring Cloud Contract Verifier in order to do the Consumer Driven Contracts approach. That way we have achieved:

  • an API that suits the consumer and the producer

  • readable contracts that were tested against the producer

  • verified stubs that can be used by all consumers in their integration tests

  • consumer-side tool that automatically downloads latest stubs and sets up stubs for you

Additional Reading

Get the Spring newsletter

Stay connected with the Spring newsletter

Subscribe

Get ahead

VMware offers training and certification to turbo-charge your progress.

Learn more

Get support

Tanzu Spring offers support and binaries for OpenJDK™, Spring, and Apache Tomcat® in one simple subscription.

Learn more

Upcoming events

Check out all the upcoming events in the Spring community.

View all