This version is still in development and is not considered stable yet. For the latest stable version, please use spring-cloud-contract 4.1.4! |
Introducing Spring Cloud Contract
Spring Cloud Contract moves TDD to the level of software architecture. It lets you perform consumer-driven and producer-driven contract testing.
History
Before becoming Spring Cloud Contract, this project was called Accurest. It was created by Marcin Grzejszczak and Jakub Kubrynski from (Codearte).
The 0.1.0
release took place on 26 Jan 2015, and it became stable with 1.0.0
release on 29 Feb 2016.
Why Do You Need It?
Assume that we have a system that consists of multiple microservices, as the following image shows:
![Microservices Architecture](../_images/Deps.png)
Testing Issues
If we want to test the application in the top left corner of the image in the preceding section to determine whether it can communicate with other services, we could do one of two things:
-
Deploy all microservices and perform end-to-end tests.
-
Mock other microservices in unit and integration tests.
Both have their advantages but also a lot of disadvantages.
Deploy all microservices and perform end-to-end tests
Advantages:
-
Simulates production.
-
Tests real communication between services.
Disadvantages:
-
To test one microservice, we have to deploy six microservices, a couple of databases, and other items.
-
The environment where the tests run is locked for a single suite of tests (nobody else would be able to run the tests in the meantime).
-
They take a long time to run.
-
The feedback comes very late in the process.
-
They are extremely hard to debug.
Mock other microservices in unit and integration tests
Advantages:
-
They provide very fast feedback.
-
They have no infrastructure requirements.
Disadvantages:
-
The implementor of the service creates stubs that might have nothing to do with reality.
-
You can go to production with passing tests and failing production.
To solve the aforementioned issues, Spring Cloud Contract was created. The main idea is to give you very fast feedback, without the need to set up the whole world of microservices. If you work on stubs, then the only applications you need are those that your application directly uses. The following image shows the relationship of stubs to an application:
![Stubbed Services](../_images/Stubs2.png)
Spring Cloud Contract gives you the certainty that the stubs that you use were created by the service that you call. Also, if you can use them, it means that they were tested against the producer’s side. In short, you can trust those stubs.
Purposes
The main purposes of Spring Cloud Contract are:
-
To ensure that HTTP and messaging stubs (used when developing the client) do exactly what the actual server-side implementation does.
-
To promote the ATDD (acceptance test-driven development) method, and the microservices architectural style.
-
To provide a way to publish changes in contracts that are immediately visible on both sides.
-
To generate boilerplate test code to be used on the server side.
By default, Spring Cloud Contract integrates with Wiremock as the HTTP server stub.
Spring Cloud Contract’s purpose is NOT to start writing business features in the contracts. Assume that we have a business use case of fraud check. If a user can be a fraud for 100 different reasons, we would assume that you would create two contracts, one for the positive case and one for the negative case. Contract tests are used to test contracts between applications, not to simulate full behavior. |
What Is a Contract?
As consumers of services, we need to define what exactly we want to achieve. We need to formulate our expectations. That is why we write contracts. In other words, a contract is an agreement on how the API or message communication should look. Consider the following example:
Assume that you want to send a request that contains the ID of a client company and the
amount it wants to borrow from us. You also want to send it to the /fraudcheck
URL by using
the PUT
method. The following listing shows a contract to check whether a client should
be marked as a fraud in both Groovy and YAML:
-
groovy
org.springframework.cloud.contract.spec.Contract.make {
request { // (1)
method 'PUT' // (2)
url '/fraudcheck' // (3)
body([ // (4)
"client.id": $(regex('[0-9]{10}')),
loanAmount : 99999
])
headers { // (5)
contentType('application/json')
}
}
response { // (6)
status OK() // (7)
body([ // (8)
fraudCheckStatus : "FRAUD",
"rejection.reason": "Amount too high"
])
headers { // (9)
contentType('application/json')
}
}
}
/*
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 `client.id` 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/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/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 `client.id` 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/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/json.*`
*/
request: # (1)
method: PUT # (2)
url: /yamlfraudcheck # (3)
body: # (4)
"client.id": 1234567890
loanAmount: 99999
headers: # (5)
Content-Type: application/json
matchers:
body:
- path: $.['client.id'] # (6)
type: by_regex
value: "[0-9]{10}"
response: # (7)
status: 200 # (8)
body: # (9)
fraudCheckStatus: "FRAUD"
"rejection.reason": "Amount too high"
headers: # (10)
Content-Type: application/json
#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 "/yamlfraudcheck"
#(4) - with the JSON body that
# * has a field `client.id`
# * has a field `loanAmount` that is equal to `99999`
#(5) - with header `Content-Type` equal to `application/json`
#(6) - and a `client.id` json entry matches the regular expression `[0-9]{10}`
#(7) - then the response will be sent with
#(8) - status equal `200`
#(9) - and JSON body equal to
# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
#(10) - with header `Content-Type` equal to `application/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 "/yamlfraudcheck"
#(4) - with the JSON body that
# * has a field `client.id` `1234567890`
# * has a field `loanAmount` that is equal to `99999`
#(5) - with header `Content-Type` equal to `application/json`
#(7) - then the test will assert if the response has been sent with
#(8) - status equal `200`
#(9) - and JSON body equal to
# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
#(10) - with header `Content-Type` equal to `application/json`
It is expected that contracts are coming from a trusted source. You should never download nor interact with contracts coming from untrusted locations. |