1493 lines
55 KiB
Plaintext
1493 lines
55 KiB
Plaintext
// Do not edit this file (e.g. go instead to src/main/asciidoc)
|
|
|
|
:branch: master
|
|
image::https://badges.gitter.im/Join%20Chat.svg[Gitter, link="https://gitter.im/spring-cloud/spring-cloud-contract?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"]
|
|
image::https://codecov.io/gh/spring-cloud/spring-cloud-contract/branch/{branch}/graph/badge.svg["codecov", link="https://codecov.io/gh/spring-cloud/spring-cloud-contract"]
|
|
image::https://circleci.com/gh/spring-cloud/spring-cloud-contract.svg?style=svg["CircleCI", link="https://circleci.com/gh/spring-cloud/spring-cloud-contract"]
|
|
:introduction_url: ../../../..
|
|
|
|
== Spring Cloud Contract
|
|
|
|
You always need confidence when pushing new features into a new application or service in
|
|
a distributed system. To that end, 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, and asserting that a contract is kept by
|
|
producers and consumers -- for both HTTP and message-based interactions.
|
|
|
|
=== Spring Cloud Contract workshops
|
|
|
|
If you prefer to learn about the project by doing some tutorials, you can check out the
|
|
workshops under
|
|
http://cloud-samples.spring.io/spring-cloud-contract-samples/workshops.html[this link].
|
|
|
|
=== Spring Cloud Contract Verifier
|
|
|
|
== Spring Cloud Contract Verifier Introduction
|
|
|
|
TIP: The Accurest project was initially started by Marcin Grzejszczak and Jakub Kubrynski
|
|
(http://codearte.io[codearte.io])
|
|
|
|
Spring Cloud Contract Verifier enables Consumer Driven Contract (CDC) development of
|
|
JVM-based applications. It moves TDD to the level of software architecture.
|
|
|
|
Spring Cloud Contract Verifier ships with _Contract Definition Language_ (CDL). Contract
|
|
definitions are used to produce the following resources:
|
|
|
|
* JSON stub definitions to be used by WireMock when doing integration testing on the
|
|
client code (_client tests_). Test code must still be written by hand, and test data is
|
|
produced by Spring Cloud Contract Verifier.
|
|
* Messaging routes, if you're using a messaging service. We integrate with Spring
|
|
Integration, Spring Cloud Stream, Spring AMQP, and Apache Camel. You can also set your
|
|
own integrations.
|
|
* Acceptance tests (in JUnit or Spock) are used to verify if server-side implementation
|
|
of the API is compliant with the contract (__server tests__). A full test is generated by
|
|
Spring Cloud Contract Verifier.
|
|
|
|
=== Why a Contract Verifier?
|
|
|
|
Assume that we have a system consisting of multiple microservices:
|
|
|
|
image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/{branch}/docs/src/main/asciidoc/images/Deps.png[Microservices Architecture]
|
|
|
|
==== Testing issues
|
|
|
|
If we wanted to test the application in top left corner 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/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 6 microservices, a couple of databases,
|
|
etc.
|
|
- 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/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 Verifier with Stub Runner 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.
|
|
|
|
image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/{branch}/docs/src/main/asciidoc/images/Stubs2.png[Stubbed Services]
|
|
|
|
Spring Cloud Contract Verifier gives you the certainty that the stubs that you use were
|
|
created by the service that you're calling. 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 Verifier with Stub Runner are:
|
|
|
|
- To ensure that WireMock/Messaging stubs (used when developing the client) do exactly
|
|
what the actual server-side implementation does.
|
|
- To promote ATDD method and 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.
|
|
|
|
IMPORTANT: Spring Cloud Contract Verifier'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 2
|
|
contracts, one for the positive case and one for the negative case. Contract tests are
|
|
used to test contracts between applications and not to simulate full behavior.
|
|
|
|
=== How It Works
|
|
|
|
This section explores how Spring Cloud Contract Verifier with Stub Runner works.
|
|
|
|
==== Defining the 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.
|
|
|
|
Assume that you want to send a request containing 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 via
|
|
the PUT method.
|
|
|
|
[source,groovy,indent=0]
|
|
----
|
|
package contracts
|
|
|
|
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 200 // (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 `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/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 `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/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.*`
|
|
*/
|
|
----
|
|
|
|
==== Client Side
|
|
|
|
Spring Cloud Contract generates stubs, which you can use during client-side testing.
|
|
You get a running WireMock instance/Messaging route that simulates the service.
|
|
You would like to feed that instance with a proper stub definition.
|
|
|
|
At some point in time, you need to send a request to the Fraud Detection service.
|
|
|
|
[source,groovy,indent=0]
|
|
----
|
|
ResponseEntity<FraudServiceResponse> response =
|
|
restTemplate.exchange("http://localhost:" + port + "/fraudcheck", HttpMethod.PUT,
|
|
new HttpEntity<>(request, httpHeaders),
|
|
FraudServiceResponse.class);
|
|
----
|
|
|
|
Annotate your test class with `@AutoConfigureStubRunner`. In the annotation provide the group id and artifact id for the Stub Runner to download stubs of your collaborators.
|
|
|
|
[source,groovy,indent=0]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@SpringBootTest(webEnvironment=WebEnvironment.NONE)
|
|
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"}, workOffline = true)
|
|
@DirtiesContext
|
|
public class LoanApplicationServiceTests {
|
|
----
|
|
|
|
After that, during the tests, Spring Cloud Contract automatically finds the stubs
|
|
(simulating the real service) in the Maven repository and exposes them on a configured
|
|
(or random) port.
|
|
|
|
==== Server Side
|
|
|
|
Since you are developing your stub, you need to be sure that it actually resembles your
|
|
concrete implementation. You cannot have a situation where your stub acts in one way and
|
|
your application behaves in a different way, especially in production.
|
|
|
|
To ensure that your application behaves the way you define in your stub, tests are
|
|
generated from the stub you provide.
|
|
|
|
The autogenerated test looks like this:
|
|
|
|
[source,java,indent=0]
|
|
----
|
|
@Test
|
|
public void validate_shouldMarkClientAsFraud() throws Exception {
|
|
// given:
|
|
MockMvcRequestSpecification request = given()
|
|
.header("Content-Type", "application/vnd.fraud.v1+json")
|
|
.body("{\"client.id\":\"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("['rejection.reason']").isEqualTo("Amount too high");
|
|
}
|
|
----
|
|
|
|
=== Step-by-step Guide to Consumer Driven Contracts (CDC)
|
|
|
|
Consider an example of Fraud Detection and the Loan Issuance process. The business
|
|
scenario is such that we want to issue loans to people but do not want them to steal from
|
|
us. The current implementation of our system grants loans to everybody.
|
|
|
|
Assume that `Loan Issuance` is a client to the `Fraud Detection` server. In the current
|
|
sprint, we must develop a new feature: if a client wants to borrow too much money, then
|
|
we mark the client as a fraud.
|
|
|
|
Technical remark - Fraud Detection has an `artifact-id` of `http-server`, while Loan
|
|
Issuance has an artifact-id of `http-client`, and both have a `group-id` of `com.example`.
|
|
|
|
Social remark - both client and server development teams need to communicate directly and
|
|
discuss changes while going through the process. CDC is all about communication.
|
|
|
|
The https://github.com/spring-cloud/spring-cloud-contract/tree/{branch}/samples/standalone/dsl/http-server[server
|
|
side code is available here] and https://github.com/spring-cloud/spring-cloud-contract/tree/{branch}/samples/standalone/dsl/http-client[the
|
|
client code here].
|
|
|
|
TIP: In this case, the producer owns the contracts. Physically, all the contract are
|
|
in the producer's repository.
|
|
|
|
==== Technical note
|
|
|
|
If using the *SNAPSHOT* / *Milestone* / *Release Candidate* versions please add the
|
|
following section to your build:
|
|
|
|
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
|
|
.Maven
|
|
----
|
|
<repositories>
|
|
<repository>
|
|
<id>spring-snapshots</id>
|
|
<name>Spring Snapshots</name>
|
|
<url>https://repo.spring.io/snapshot</url>
|
|
<snapshots>
|
|
<enabled>true</enabled>
|
|
</snapshots>
|
|
</repository>
|
|
<repository>
|
|
<id>spring-milestones</id>
|
|
<name>Spring Milestones</name>
|
|
<url>https://repo.spring.io/milestone</url>
|
|
<snapshots>
|
|
<enabled>false</enabled>
|
|
</snapshots>
|
|
</repository>
|
|
<repository>
|
|
<id>spring-releases</id>
|
|
<name>Spring Releases</name>
|
|
<url>https://repo.spring.io/release</url>
|
|
<snapshots>
|
|
<enabled>false</enabled>
|
|
</snapshots>
|
|
</repository>
|
|
</repositories>
|
|
<pluginRepositories>
|
|
<pluginRepository>
|
|
<id>spring-snapshots</id>
|
|
<name>Spring Snapshots</name>
|
|
<url>https://repo.spring.io/snapshot</url>
|
|
<snapshots>
|
|
<enabled>true</enabled>
|
|
</snapshots>
|
|
</pluginRepository>
|
|
<pluginRepository>
|
|
<id>spring-milestones</id>
|
|
<name>Spring Milestones</name>
|
|
<url>https://repo.spring.io/milestone</url>
|
|
<snapshots>
|
|
<enabled>false</enabled>
|
|
</snapshots>
|
|
</pluginRepository>
|
|
<pluginRepository>
|
|
<id>spring-releases</id>
|
|
<name>Spring Releases</name>
|
|
<url>https://repo.spring.io/release</url>
|
|
<snapshots>
|
|
<enabled>false</enabled>
|
|
</snapshots>
|
|
</pluginRepository>
|
|
</pluginRepositories>
|
|
----
|
|
|
|
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
|
.Gradle
|
|
----
|
|
repositories {
|
|
mavenCentral()
|
|
mavenLocal()
|
|
maven { url "http://repo.spring.io/snapshot" }
|
|
maven { url "http://repo.spring.io/milestone" }
|
|
maven { url "http://repo.spring.io/release" }
|
|
}
|
|
----
|
|
|
|
==== Consumer side (Loan Issuance)
|
|
|
|
As a developer of the Loan Issuance service (a consumer of the Fraud Detection server), you might do the following steps:
|
|
|
|
. Start doing TDD by writing a test for your feature.
|
|
. Write the missing implementation.
|
|
. Clone the Fraud Detection service repository locally.
|
|
. Define the contract locally in the repo of Fraud Detection service.
|
|
. Add the Spring Cloud Contract Verifier plugin.
|
|
. Run the integration tests.
|
|
. File a pull request.
|
|
. Create an initial implementation.
|
|
. Take over the pull request.
|
|
. Write the missing implementation.
|
|
. Deploy your app.
|
|
. Work online.
|
|
|
|
*Start doing TDD by writing a test for your feature.*
|
|
|
|
[source,groovy,indent=0]
|
|
----
|
|
@Test
|
|
public void shouldBeRejectedDueToAbnormalLoanAmount() {
|
|
// given:
|
|
LoanApplication application = new LoanApplication(new Client("1234567890"),
|
|
99999);
|
|
// when:
|
|
LoanApplicationResult loanApplication = service.loanApplication(application);
|
|
// then:
|
|
assertThat(loanApplication.getLoanApplicationStatus())
|
|
.isEqualTo(LoanApplicationStatus.LOAN_APPLICATION_REJECTED);
|
|
assertThat(loanApplication.getRejectionReason()).isEqualTo("Amount too high");
|
|
}
|
|
----
|
|
|
|
Assume that you have written a test of your new feature. If a loan application for a big
|
|
amount is received, the system 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. Assume
|
|
that you need to send the request containing the ID of the client and the amount the
|
|
client wants to borrow. You want to send it to the `/fraudcheck` url via the `PUT` method.
|
|
|
|
[source,groovy,indent=0]
|
|
----
|
|
ResponseEntity<FraudServiceResponse> response =
|
|
restTemplate.exchange("http://localhost:" + port + "/fraudcheck", HttpMethod.PUT,
|
|
new HttpEntity<>(request, httpHeaders),
|
|
FraudServiceResponse.class);
|
|
----
|
|
|
|
For simplicity, the port of the Fraud Detection service is set to `8080`, and the
|
|
application runs on `8090`.
|
|
|
|
If you start the test at this point, it breaks, because no service currently runs on port
|
|
`8080`.
|
|
|
|
*Clone the Fraud Detection service repository locally.*
|
|
|
|
You can start by playing around with the server side contract. To do so, you must first
|
|
clone it.
|
|
|
|
[source,bash,indent=0]
|
|
----
|
|
git clone https://your-git-server.com/server-side.git local-http-server-repo
|
|
----
|
|
|
|
*Define the contract locally in the repo of Fraud Detection service.*
|
|
|
|
As a consumer, you need to define what exactly you want to achieve. You need to formulate
|
|
your expectations. To do so, write the following contract:
|
|
|
|
IMPORTANT: Place the contract under `src/test/resources/contracts/fraud` folder. The `fraud` folder
|
|
is important because the producer's test base class name references that folder.
|
|
|
|
[source,groovy,indent=0]
|
|
----
|
|
package contracts
|
|
|
|
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 200 // (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 `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/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 `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/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.*`
|
|
*/
|
|
----
|
|
|
|
The Contract is written using a statically typed Groovy DSL. You might wonder what about
|
|
those `value(client(...), server(...))` parts. By using this notation, Spring Cloud
|
|
Contract lets you define parts of a JSON block, a URL, etc., which are dynamic. In case
|
|
of an identifier or a timestamp, you need not hardcode a value. You want to allow some
|
|
different ranges of values. To enable ranges of values, you can set regular expressions
|
|
matching those values for the consumer side. You can provide the body by means of either
|
|
a map notation or String with interpolations.
|
|
https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract.html#_contract_dsl[Consult the docs
|
|
for more information.] We highly recommend using the map notation!
|
|
|
|
TIP: You must understand the map notation in order to set up contracts. Please read the
|
|
http://groovy-lang.org/json.html[Groovy docs regarding JSON].
|
|
|
|
The previously shown contract is an agreement between two sides that:
|
|
|
|
- if an HTTP request is sent with all of
|
|
** a `PUT` method on the `/fraudcheck` endpoint,
|
|
** a JSON body with a `client.id` that matches the regular expression `[0-9]{10}` and
|
|
`loanAmount` equal to `99999`,
|
|
** and a `Content-Type` header with a value of `application/vnd.fraud.v1+json`,
|
|
- then an HTTP response is sent to the consumer that
|
|
** has status `200`,
|
|
** contains a 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 you are ready to check the API in practice in the integration tests, you need to
|
|
install the stubs locally.
|
|
|
|
*Add the Spring Cloud Contract Verifier plugin.*
|
|
|
|
We can add either a Maven or a Gradle plugin. In this example, you see how to add Maven.
|
|
First, add the `Spring Cloud Contract` BOM.
|
|
|
|
[source,xml,indent=0]
|
|
----
|
|
<dependencyManagement>
|
|
<dependencies>
|
|
<dependency>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-dependencies</artifactId>
|
|
<version>${spring-cloud-dependencies.version}</version>
|
|
<type>pom</type>
|
|
<scope>import</scope>
|
|
</dependency>
|
|
</dependencies>
|
|
</dependencyManagement>
|
|
----
|
|
|
|
Next, add the `Spring Cloud Contract Verifier` Maven plugin
|
|
|
|
[source,xml,indent=0]
|
|
----
|
|
<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.fraud</packageWithBaseClasses>
|
|
</configuration>
|
|
</plugin>
|
|
----
|
|
|
|
Since the plugin was added, you get the `Spring Cloud Contract Verifier` features which,
|
|
from the provided contracts:
|
|
|
|
- generate and run tests
|
|
- produce and install stubs
|
|
|
|
You do not want to generate tests since you, as the consumer, want only to play with the
|
|
stubs. You need to skip the test generation and execution. When you execute:
|
|
|
|
[source,bash,indent=0]
|
|
----
|
|
cd local-http-server-repo
|
|
./mvnw clean install -DskipTests
|
|
----
|
|
|
|
In the logs, you see something like this:
|
|
|
|
[source,bash,indent=0]
|
|
----
|
|
[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.5.5.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
|
|
----
|
|
|
|
The following line is extremely important:
|
|
|
|
[source,bash,indent=0]
|
|
----
|
|
[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 confirms 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 must do the following in your consumer side project (`Loan
|
|
Application service`):
|
|
|
|
Add the `Spring Cloud Contract` BOM:
|
|
|
|
[source,xml,indent=0]
|
|
----
|
|
<dependencyManagement>
|
|
<dependencies>
|
|
<dependency>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-dependencies</artifactId>
|
|
<version>${spring-cloud-dependencies.version}</version>
|
|
<type>pom</type>
|
|
<scope>import</scope>
|
|
</dependency>
|
|
</dependencies>
|
|
</dependencyManagement>
|
|
----
|
|
|
|
Add the dependency to `Spring Cloud Contract Stub Runner`:
|
|
|
|
[source,xml,indent=0]
|
|
----
|
|
<dependency>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
----
|
|
|
|
Annotate your test class with `@AutoConfigureStubRunner`. In the annotation, provide the
|
|
`group-id` and `artifact-id` for the Stub Runner to download the stubs of your
|
|
collaborators. (Optional step) Because you're playing with the collaborators offline, you
|
|
can also provide the offline work switch.
|
|
|
|
[source,groovy,indent=0]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@SpringBootTest(webEnvironment=WebEnvironment.NONE)
|
|
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"}, workOffline = true)
|
|
@DirtiesContext
|
|
public class LoanApplicationServiceTests {
|
|
----
|
|
|
|
Now, when you run your tests, you see something like this:
|
|
|
|
[source,bash,indent=0]
|
|
----
|
|
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}]
|
|
----
|
|
|
|
This output means that Stub Runner has found your stubs and started a server for your 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 pull request.*
|
|
|
|
What you have done until now is an iterative process. You can play around with the
|
|
contract, install it locally, and work on the consumer side until the contract works as
|
|
you wish.
|
|
|
|
Once you are satisfied with the results and the test passes, publish a pull request to
|
|
the server side. Currently, the consumer side work is done.
|
|
|
|
==== Producer side (Fraud Detection server)
|
|
|
|
As a developer of the Fraud Detection server (a server to the Loan Issuance service):
|
|
|
|
*Create an initial implementation.*
|
|
|
|
As a reminder, you can see the initial implementation here:
|
|
|
|
[source,java,indent=0]
|
|
----
|
|
@RequestMapping(value = "/fraudcheck", method = PUT)
|
|
public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
|
|
return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);
|
|
}
|
|
----
|
|
|
|
*Take over the pull request.*
|
|
|
|
[source,bash,indent=0]
|
|
----
|
|
git checkout -b contract-change-pr master
|
|
git pull https://your-git-server.com/server-side-fork.git contract-change-pr
|
|
----
|
|
|
|
You must add the dependencies needed by the autogenerated tests:
|
|
|
|
[source,xml,indent=0]
|
|
----
|
|
<dependency>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
----
|
|
|
|
In the configuration of the Maven plugin, pass the `packageWithBaseClasses` property
|
|
|
|
[source,xml,indent=0]
|
|
----
|
|
<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.fraud</packageWithBaseClasses>
|
|
</configuration>
|
|
</plugin>
|
|
----
|
|
|
|
IMPORTANT: This example uses "convention based" naming by setting the
|
|
`packageWithBaseClasses` property. Doing so means that the two last packages combine to
|
|
make the name of the base test class. In our case, the contracts were placed under
|
|
`src/test/resources/contracts/fraud`. Since you do not have two packages starting from
|
|
the `contracts` folder, pick only one, which should be `fraud`. Add the `Base` suffix and
|
|
capitalize `fraud`. That gives you the `FraudBase` test class name.
|
|
|
|
All the generated tests extend that class. Over there, you can set up your Spring Context
|
|
or whatever is necessary. In this case, use http://rest-assured.io/[Rest Assured MVC] to
|
|
start the server side `FraudDetectionController`.
|
|
|
|
[source,java,indent=0]
|
|
----
|
|
package com.example.fraud;
|
|
|
|
import org.junit.Before;
|
|
|
|
import io.restassured.module.mockmvc.RestAssuredMockMvc;
|
|
|
|
public class FraudBase {
|
|
@Before
|
|
public void setup() {
|
|
RestAssuredMockMvc.standaloneSetup(new FraudDetectionController(),
|
|
new FraudStatsController(stubbedStatsProvider()));
|
|
}
|
|
|
|
private StatsProvider stubbedStatsProvider() {
|
|
return fraudType -> {
|
|
switch (fraudType) {
|
|
case DRUNKS:
|
|
return 100;
|
|
case ALL:
|
|
return 200;
|
|
}
|
|
return 0;
|
|
};
|
|
}
|
|
|
|
public void assertThatRejectionReasonIsNull(Object rejectionReason) {
|
|
assert rejectionReason == null;
|
|
}
|
|
}
|
|
----
|
|
|
|
Now, if you run the `./mvnw clean install`, you get something like this:
|
|
|
|
[source,bash,indent=0]
|
|
----
|
|
Results :
|
|
|
|
Tests in error:
|
|
ContractVerifierTest.validate_shouldMarkClientAsFraud:32 » IllegalState Parsed...
|
|
----
|
|
|
|
This error occurs because you have a new contract from which a test was generated and it
|
|
failed since you have not implemented the feature. The auto-generated test would look
|
|
like this:
|
|
|
|
[source,java,indent=0]
|
|
----
|
|
@Test
|
|
public void validate_shouldMarkClientAsFraud() throws Exception {
|
|
// given:
|
|
MockMvcRequestSpecification request = given()
|
|
.header("Content-Type", "application/vnd.fraud.v1+json")
|
|
.body("{\"client.id\":\"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("['rejection.reason']").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.
|
|
|
|
Note that, on the producer side, you are also doing TDD. The expectations are expressed
|
|
in the form of a test. This test sends a request to our own application with the URL,
|
|
headers, and body defined in the contract. It also is expecting precisely defined values
|
|
in the response. In other words, you have the `red` part of `red`, `green`, and
|
|
`refactor`. It is time to convert the `red` into the `green`.
|
|
|
|
*Write the missing implementation.*
|
|
|
|
Because you know the expected input and expected output, you can write the missing
|
|
implementation:
|
|
|
|
[source,java,indent=0]
|
|
----
|
|
@RequestMapping(value = "/fraudcheck", method = PUT)
|
|
public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
|
|
if (amountGreaterThanThreshold(fraudCheck)) {
|
|
return new FraudCheckResult(FraudCheckStatus.FRAUD, AMOUNT_TOO_HIGH);
|
|
}
|
|
return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);
|
|
}
|
|
----
|
|
|
|
When you execute `./mvnw clean install` again, the tests 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 finish your work, you can deploy your change. First, merge the branch:
|
|
|
|
[source,bash,indent=0]
|
|
----
|
|
git checkout master
|
|
git merge --no-ff contract-change-pr
|
|
git push origin master
|
|
----
|
|
|
|
Your CI might run something like `./mvnw clean deploy`, which would publish both the
|
|
application and the stub artifacts.
|
|
|
|
==== Consumer Side (Loan Issuance) Final Step
|
|
|
|
As a developer of the Loan Issuance service (a consumer of the Fraud Detection server):
|
|
|
|
*Merge branch to master.*
|
|
|
|
[source,bash,indent=0]
|
|
----
|
|
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 and indicate
|
|
where the repository with your stubs is located. At this moment the stubs of the server
|
|
side are automatically downloaded from Nexus/Artifactory. You can switch off the value of
|
|
the `workOffline` parameter in your annotation. The following code shows an example of
|
|
achieving the same thing by changing the properties.
|
|
|
|
[source,yaml,indent=0]
|
|
----
|
|
stubrunner:
|
|
ids: 'com.example:http-server-dsl:+:stubs:8080'
|
|
repositoryRoot: http://repo.spring.io/libs-snapshot
|
|
----
|
|
|
|
That's it!
|
|
|
|
=== Dependencies
|
|
|
|
The best way to add dependencies is to use the proper `starter` dependency.
|
|
|
|
For `stub-runner`, use `spring-cloud-starter-stub-runner`. When you use a plugin, add
|
|
`spring-cloud-starter-contract-verifier`.
|
|
|
|
=== Additional Links
|
|
|
|
Here are some resources related to Spring Cloud Contract Verifier and Stub Runner. Note
|
|
that some may be outdated, because the Spring Cloud Contract Verifier project is under
|
|
constant development.
|
|
|
|
==== Spring Cloud Contract video
|
|
|
|
You can check out the video from the Warsaw JUG about Spring Cloud Contract:
|
|
|
|
video::sAAklvxmPmk[youtube,start=538,width=640,height=480]
|
|
|
|
==== Readings
|
|
|
|
- http://www.slideshare.net/MarcinGrzejszczak/stick-to-the-rules-consumer-driven-contracts-201507-confitura[Slides from Marcin Grzejszczak's talk about Accurest]
|
|
- http://toomuchcoding.com/blog/categories/accurest/[Accurest related articles from Marcin Grzejszczak's blog]
|
|
- http://toomuchcoding.com/blog/categories/spring-cloud-contract/[Spring Cloud Contract related articles from Marcin Grzejszczak's blog]
|
|
- http://groovy-lang.org/json.html[Groovy docs regarding JSON]
|
|
|
|
=== Samples
|
|
|
|
You can find some samples at
|
|
https://github.com/spring-cloud-samples/spring-cloud-contract-samples[samples].
|
|
|
|
== Links
|
|
|
|
The following links may be helpful when working with Spring Cloud Contract Verifier:
|
|
|
|
* https://github.com/spring-cloud/spring-cloud-contract/[Spring Cloud Contract Github
|
|
Repository]
|
|
* https://github.com/spring-cloud-samples/spring-cloud-contract-samples/[Spring Cloud
|
|
Contract Samples]
|
|
* https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract.html[Spring Cloud
|
|
Contract Documentation]
|
|
* https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract.html/deprecated[Accurest
|
|
Legacy Documentation]
|
|
* https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract.html/#spring-cloud-contract-stub-runner[Spring
|
|
Cloud Contract Stub Runner Documentation]
|
|
* https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract.html/#stub-runner-for-messaging[Spring
|
|
Cloud Contract Stub Runner Messaging Documentation]
|
|
* https://gitter.im/spring-cloud/spring-cloud-contract[Spring Cloud Contract Gitter]
|
|
* https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract-maven-plugin/[Spring
|
|
Cloud Contract Maven Plugin]
|
|
* https://www.youtube.com/watch?v=sAAklvxmPmk[Spring Cloud Contract WJUG Presentation by
|
|
Marcin Grzejszczak]
|
|
|
|
=== Spring Cloud Contract WireMock
|
|
|
|
:core_path: ../../../..
|
|
:doc_samples: {core_path}/samples/wiremock-jetty
|
|
:wiremock_tests: {core_path}/spring-cloud-contract-wiremock
|
|
|
|
== Spring Cloud Contract WireMock
|
|
|
|
The Spring Cloud Contract WireMock modules let you use http://wiremock.org[WireMock] in a
|
|
Spring Boot application. Check out the
|
|
https://github.com/spring-cloud/spring-cloud-contract/tree/{branch}/samples[samples]
|
|
for more details.
|
|
|
|
If you have a Spring Boot application that uses Tomcat as an embedded server (which is
|
|
the default with `spring-boot-starter-web`), you can add
|
|
`spring-cloud-contract-wiremock` to your classpath and add `@AutoConfigureWireMock` in
|
|
order to be able to use Wiremock in your tests. Wiremock runs as a stub server and you
|
|
can register stub behavior using a Java API or via static JSON declarations as part of
|
|
your test. The following code shows an example:
|
|
|
|
[source,java,indent=0]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
|
@AutoConfigureWireMock(port = 0)
|
|
public class WiremockForDocsTests {
|
|
// A service that calls out over HTTP
|
|
@Autowired private Service service;
|
|
|
|
// Using the WireMock APIs in the normal way:
|
|
@Test
|
|
public void contextLoads() throws Exception {
|
|
// Stubbing WireMock
|
|
stubFor(get(urlEqualTo("/resource"))
|
|
.willReturn(aResponse().withHeader("Content-Type", "text/plain").withBody("Hello World!")));
|
|
// We're asserting if WireMock responded properly
|
|
assertThat(this.service.go()).isEqualTo("Hello World!");
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
To start the stub server on a different port use (for example),
|
|
`@AutoConfigureWireMock(port=9999)`. For a random port, use a value of `0`. The stub
|
|
server port can be bound in the test application context with the "wiremock.server.port"
|
|
property. Using `@AutoConfigureWireMock` adds a bean of type `WiremockConfiguration` to
|
|
your test application context, where it will be cached in between methods and classes
|
|
having the same context, the same as for Spring integration tests.
|
|
|
|
=== Registering Stubs Automatically
|
|
|
|
If you use `@AutoConfigureWireMock`, it registers WireMock JSON stubs from the file
|
|
system or classpath (by default, from `file:src/test/resources/mappings`). You can
|
|
customize the locations using the `stubs` attribute in the annotation, which can be an
|
|
Ant-style resource pattern or a directory. In the case of a directory, `**/*.json` is
|
|
appended. The following code shows an example:
|
|
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@SpringBootTest
|
|
@AutoConfigureWireMock(stubs="classpath:/stubs")
|
|
public class WiremockImportApplicationTests {
|
|
|
|
@Autowired
|
|
private Service service;
|
|
|
|
@Test
|
|
public void contextLoads() throws Exception {
|
|
assertThat(this.service.go()).isEqualTo("Hello World!");
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
NOTE: Actually, WireMock always loads mappings from `src/test/resources/mappings` *as
|
|
well as* the custom locations in the stubs attribute. To change this behavior, you can
|
|
also specify a files root as described in the next section of this document.
|
|
|
|
=== Using Files to Specify the Stub Bodies
|
|
|
|
WireMock can read response bodies from files on the classpath or the file system. In that
|
|
case, you can see in the JSON DSL that the response has a `bodyFileName` instead of a
|
|
(literal) `body`. The files are resolved relative to a root directory (by default,
|
|
`src/test/resources/\__files`). To customize this location you can set the `files`
|
|
attribute in the `@AutoConfigureWireMock` annotation to the location of the parent
|
|
directory (in other words, `__files` is a subdirectory). You can use Spring resource
|
|
notation to refer to `file:...` or `classpath:...` locations. Generic URLs are not
|
|
supported. A list of values can be given, in which case WireMock resolves the first file
|
|
that exists when it needs to find a response body.
|
|
|
|
NOTE: When you configure the `files` root, it also affects the
|
|
automatic loading of stubs, because they come from the root location
|
|
in a subdirectory called "mappings". The value of `files` has no
|
|
effect on the stubs loaded explicitly from the `stubs` attribute.
|
|
|
|
=== Alternative: Using JUnit Rules
|
|
|
|
For a more conventional WireMock experience, you can use JUnit `@Rules` to start and stop
|
|
the server. To do so, use the `WireMockSpring` convenience class to obtain an `Options`
|
|
instance, as shown in the followin example:
|
|
|
|
[source,java,indent=0]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
|
public class WiremockForDocsClassRuleTests {
|
|
|
|
// Start WireMock on some dynamic port
|
|
// for some reason `dynamicPort()` is not working properly
|
|
@ClassRule
|
|
public static WireMockClassRule wiremock = new WireMockClassRule(
|
|
WireMockSpring.options().dynamicPort());
|
|
// A service that calls out over HTTP to localhost:${wiremock.port}
|
|
@Autowired
|
|
private Service service;
|
|
|
|
// Using the WireMock APIs in the normal way:
|
|
@Test
|
|
public void contextLoads() throws Exception {
|
|
// Stubbing WireMock
|
|
wiremock.stubFor(get(urlEqualTo("/resource"))
|
|
.willReturn(aResponse().withHeader("Content-Type", "text/plain").withBody("Hello World!")));
|
|
// We're asserting if WireMock responded properly
|
|
assertThat(this.service.go()).isEqualTo("Hello World!");
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
The `@ClassRule` means that the server shuts down after all the methods in this class
|
|
have been run.
|
|
|
|
=== Relaxed SSL Validation for Rest Template
|
|
|
|
WireMock lets you stub a "secure" server with an "https" URL protocol. If your
|
|
application wants to contact that stub server in an integration test, it will find that
|
|
the SSL certificates are not valid (the usual problem with self-installed certificates).
|
|
The best option is often to re-configure the client to use "http". If that's not an
|
|
option, you can ask Spring to configure an HTTP client that ignores SSL validation errors
|
|
(do so only for tests, of course).
|
|
|
|
To make this work with minimum fuss, you need to be using the Spring Boot
|
|
`RestTemplateBuilder` in your app, as shown in the following example:
|
|
|
|
[source,java,indent=0]
|
|
----
|
|
@Bean
|
|
public RestTemplate restTemplate(RestTemplateBuilder builder) {
|
|
return builder.build();
|
|
}
|
|
----
|
|
|
|
You need `RestTemplateBuilder` because the builder is passed through callbacks to
|
|
initialize it, so the SSL validation can be set up in the client at that point. This
|
|
happens automatically in your test if you are using the `@AutoConfigureWireMock`
|
|
annotation or the stub runner. If you use the JUnit `@Rule` approach, you need to add the
|
|
`@AutoConfigureHttpClient` annotation as well, as shown in the following example:
|
|
|
|
[source,java,indent=0]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@SpringBootTest("app.baseUrl=https://localhost:6443")
|
|
@AutoConfigureHttpClient
|
|
public class WiremockHttpsServerApplicationTests {
|
|
|
|
@ClassRule
|
|
public static WireMockClassRule wiremock = new WireMockClassRule(
|
|
WireMockSpring.options().httpsPort(6443));
|
|
...
|
|
}
|
|
----
|
|
|
|
If you are using `spring-boot-starter-test`, you have the Apache HTTP client on the
|
|
classpath and it is selected by the `RestTemplateBuilder` and configured to ignore SSL
|
|
errors. If you use the default `java.net` client, you do not need the annotation (but it
|
|
won't do any harm). There is no support currently for other clients, but it may be added
|
|
in future releases.
|
|
|
|
=== WireMock and Spring MVC Mocks
|
|
|
|
Spring Cloud Contract provides a convenience class that can load JSON WireMock stubs into
|
|
a Spring `MockRestServiceServer`. The following code shows an example:
|
|
|
|
[source,java,indent=0]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
|
|
public class WiremockForDocsMockServerApplicationTests {
|
|
|
|
@Autowired
|
|
private RestTemplate restTemplate;
|
|
|
|
@Autowired
|
|
private Service service;
|
|
|
|
@Test
|
|
public void contextLoads() throws Exception {
|
|
// will read stubs classpath
|
|
MockRestServiceServer server = WireMockRestServiceServer.with(this.restTemplate)
|
|
.baseUrl("http://example.org").stubs("classpath:/stubs/resource.json")
|
|
.build();
|
|
// We're asserting if WireMock responded properly
|
|
assertThat(this.service.go()).isEqualTo("Hello World");
|
|
server.verify();
|
|
}
|
|
}
|
|
----
|
|
|
|
The `baseUrl` value is prepended to all mock calls, and the `stubs()` method takes a stub
|
|
path resource pattern as an argument. In the preceding example, the stub defined at
|
|
`/stubs/resource.json` is loaded into the mock server. If the `RestTemplate` is asked to
|
|
visit `http://example.org/`, it gets the responses as being declared at that URL. More
|
|
than one stub pattern can be specified, and each one can be a directory (for a recursive
|
|
list of all ".json"), a fixed filename (as in the example above), or an Ant-style
|
|
pattern. The JSON format is the normal WireMock format, which you can read about in the
|
|
http://wiremock.org/docs/stubbing/[WireMock website].
|
|
|
|
Currently, the Spring Cloud Contract Verifier supports Tomcat, Jetty, and Undertow as
|
|
Spring Boot embedded servers, and Wiremock itself has "native" support for a particular
|
|
version of Jetty (currently 9.2). To use the native Jetty, you need to add the native
|
|
Wiremock dependencies and exclude the Spring Boot container (if there is one).
|
|
|
|
=== Generating Stubs using REST Docs
|
|
|
|
https://projects.spring.io/spring-restdocs[Spring REST Docs] can be used to generate
|
|
documentation (for example in Asciidoctor format) for an HTTP API with Spring MockMvc or
|
|
Rest Assured. At the same time that you generate documentation for your API, you can also
|
|
generate WireMock stubs by using Spring Cloud Contract WireMock. To do so, write your
|
|
normal REST Docs test cases and use `@AutoConfigureRestDocs` to have stubs be
|
|
automatically generated in the REST Docs output directory. The following code shows an
|
|
example:
|
|
|
|
[source,java,indent=0]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@SpringBootTest
|
|
@AutoConfigureRestDocs(outputDir = "target/snippets")
|
|
@AutoConfigureMockMvc
|
|
public class ApplicationTests {
|
|
|
|
@Autowired
|
|
private MockMvc mockMvc;
|
|
|
|
@Test
|
|
public void contextLoads() throws Exception {
|
|
mockMvc.perform(get("/resource"))
|
|
.andExpect(content().string("Hello World"))
|
|
.andDo(document("resource"));
|
|
}
|
|
}
|
|
----
|
|
|
|
This test generates a WireMock stub at "target/snippets/stubs/resource.json". It matches
|
|
all GET requests to the "/resource" path.
|
|
|
|
Without any additional configuration, this tests creates a stub with a request matcher
|
|
for the HTTP method and all headers except "host" and "content-length". To match the
|
|
request more precisely (for example, to match the body of a POST or PUT), we need to
|
|
explicitly create a request matcher. Doing so has two effects:
|
|
|
|
* Creating a stub that matches only in the way you specify.
|
|
* Asserting that the request in the test case also matches the same conditions.
|
|
|
|
The main entry point for this feature is `WireMockRestDocs.verify()`, which can be used
|
|
as a substitute for the `document()` convenience method, as shown in the following
|
|
example:
|
|
|
|
[source,java,indent=0]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@SpringBootTest
|
|
@AutoConfigureRestDocs(outputDir = "target/snippets")
|
|
@AutoConfigureMockMvc
|
|
public class ApplicationTests {
|
|
|
|
@Autowired
|
|
private MockMvc mockMvc;
|
|
|
|
@Test
|
|
public void contextLoads() throws Exception {
|
|
mockMvc.perform(post("/resource")
|
|
.content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
|
|
.andExpect(status().isOk())
|
|
.andDo(verify().jsonPath("$.id")
|
|
.stub("resource"));
|
|
}
|
|
}
|
|
----
|
|
|
|
This contract specifies that any valid POST with an "id" field receives the response
|
|
defined in this test. You can chain together calls to `.jsonPath()` to add additional
|
|
matchers. If JSON Path is unfamiliar, The https://github.com/jayway/JsonPath[JayWay
|
|
documentation] can help you get up to speed.
|
|
|
|
Instead of the `jsonPath` and `contentType` convenience methods, you can also use the
|
|
WireMock APIs to verify that the request matches the created stub, as shown in the
|
|
following example:
|
|
|
|
[source,java,indent=0]
|
|
----
|
|
@Test
|
|
public void contextLoads() throws Exception {
|
|
mockMvc.perform(post("/resource")
|
|
.content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
|
|
.andExpect(status().isOk())
|
|
.andDo(verify()
|
|
.wiremock(WireMock.post(
|
|
urlPathEquals("/resource"))
|
|
.withRequestBody(matchingJsonPath("$.id"))
|
|
.stub("post-resource"));
|
|
}
|
|
----
|
|
|
|
The WireMock API is rich. You can match headers, query parameters, and request body by
|
|
regex as well as by JSON path. These features can be used to create stubs with a wider
|
|
range of parameters. The above example generates a stub resembling the following example:
|
|
|
|
.post-resource.json
|
|
[source,json]
|
|
----
|
|
{
|
|
"request" : {
|
|
"url" : "/resource",
|
|
"method" : "POST",
|
|
"bodyPatterns" : [ {
|
|
"matchesJsonPath" : "$.id"
|
|
}]
|
|
},
|
|
"response" : {
|
|
"status" : 200,
|
|
"body" : "Hello World",
|
|
"headers" : {
|
|
"X-Application-Context" : "application:-1",
|
|
"Content-Type" : "text/plain"
|
|
}
|
|
}
|
|
}
|
|
----
|
|
|
|
NOTE: You can use either the `wiremock()` method or the `jsonPath()` and `contentType()`
|
|
methods to create request matchers, but you can't use both approaches.
|
|
|
|
On the consumer side, you can make the `resource.json` generated earlier in this section
|
|
available on the classpath (by
|
|
https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract.html#_publishing_stubs_as_jars[publishing
|
|
stubs as JARs], for example). After that, you can create a stub using WireMock in a
|
|
number of different ways, including by using
|
|
`@AutoConfigureWireMock(stubs="classpath:resource.json")`, as described earlier in this
|
|
document.
|
|
|
|
=== Generating Contracts by Using REST Docs
|
|
|
|
You can also generate Spring Cloud Contract DSL files and documentation with Spring REST
|
|
Docs. If you do so in combination with Spring Cloud WireMock, you get both the contracts
|
|
and the stubs.
|
|
|
|
Why would you want to use this feature? Some people in the community asked questions
|
|
about a situation in which they would like to move to DSL-based contract definition,
|
|
but they already have a lot of Spring MVC tests. Using this feature lets you generate
|
|
the contract files that you can later modify and move to folders (defined in your
|
|
configuration) so that the plugin finds them.
|
|
|
|
TIP: You might wonder why this functionality is in the WireMock module. The functionality
|
|
is there because it makes sense to generate both the contracts and the stubs.
|
|
|
|
Consider the following test:
|
|
|
|
[source,java]
|
|
----
|
|
this.mockMvc.perform(post("/foo")
|
|
.accept(MediaType.APPLICATION_PDF)
|
|
.accept(MediaType.APPLICATION_JSON)
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content("{\"foo\": 23 }"))
|
|
.andExpect(status().isOk())
|
|
.andExpect(content().string("bar"))
|
|
// first WireMock
|
|
.andDo(WireMockRestDocs.verify()
|
|
.jsonPath("$[?(@.foo >= 20)]")
|
|
.contentType(MediaType.valueOf("application/json"))
|
|
.stub("shouldGrantABeerIfOldEnough"))
|
|
// then Contract DSL documentation
|
|
.andDo(document("index", SpringCloudContractRestDocs.dslContract()));
|
|
----
|
|
|
|
The preceding test creates the stub presented in the previous section, generating both
|
|
the contract and a documentation file.
|
|
|
|
The contract is called `index.groovy` and might look like the following example:
|
|
|
|
[source,groovy]
|
|
----
|
|
import org.springframework.cloud.contract.spec.Contract
|
|
|
|
Contract.make {
|
|
request {
|
|
method 'POST'
|
|
url '/foo'
|
|
body('''
|
|
{"foo": 23 }
|
|
''')
|
|
headers {
|
|
header('''Accept''', '''application/json''')
|
|
header('''Content-Type''', '''application/json''')
|
|
}
|
|
}
|
|
response {
|
|
status 200
|
|
body('''
|
|
bar
|
|
''')
|
|
headers {
|
|
header('''Content-Type''', '''application/json;charset=UTF-8''')
|
|
header('''Content-Length''', '''3''')
|
|
}
|
|
testMatchers {
|
|
jsonPath('$[?(@.foo >= 20)]', byType())
|
|
}
|
|
}
|
|
}
|
|
----
|
|
|
|
The generated document (formatted in Asciidoc in this case) contains a formatted
|
|
contract. The location of this file would be `index/dsl-contract.adoc`.
|
|
|
|
== Documentation
|
|
|
|
You can read more about Spring Cloud Contract Verifier by reading the
|
|
{documentation_url}[docs]
|
|
|
|
== Contributing
|
|
|
|
Spring Cloud is released under the non-restrictive Apache 2.0 license,
|
|
and follows a very standard Github development process, using Github
|
|
tracker for issues and merging pull requests into master. If you want
|
|
to contribute even something trivial please do not hesitate, but
|
|
follow the guidelines below.
|
|
|
|
=== Sign the Contributor License Agreement
|
|
Before we accept a non-trivial patch or pull request we will need you to sign the
|
|
https://cla.pivotal.io/sign/spring[Contributor License Agreement].
|
|
Signing the contributor's agreement does not grant anyone commit rights to the main
|
|
repository, but it does mean that we can accept your contributions, and you will get an
|
|
author credit if we do. Active contributors might be asked to join the core team, and
|
|
given the ability to merge pull requests.
|
|
|
|
=== Code of Conduct
|
|
This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/master/docs/src/main/asciidoc/code-of-conduct.adoc[code of
|
|
conduct]. By participating, you are expected to uphold this code. Please report
|
|
unacceptable behavior to spring-code-of-conduct@pivotal.io.
|
|
|
|
=== Code Conventions and Housekeeping
|
|
None of these is essential for a pull request, but they will all help. They can also be
|
|
added after the original pull request but before a merge.
|
|
|
|
* Use the Spring Framework code format conventions. If you use Eclipse
|
|
you can import formatter settings using the
|
|
`eclipse-code-formatter.xml` file from the
|
|
https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml[Spring
|
|
Cloud Build] project. If using IntelliJ, you can use the
|
|
http://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter
|
|
Plugin] to import the same file.
|
|
* Make sure all new `.java` files to have a simple Javadoc class comment with at least an
|
|
`@author` tag identifying you, and preferably at least a paragraph on what the class is
|
|
for.
|
|
* Add the ASF license header comment to all new `.java` files (copy from existing files
|
|
in the project)
|
|
* Add yourself as an `@author` to the .java files that you modify substantially (more
|
|
than cosmetic changes).
|
|
* Add some Javadocs and, if you change the namespace, some XSD doc elements.
|
|
* A few unit tests would help a lot as well -- someone has to do it.
|
|
* If no-one else is using your branch, please rebase it against the current master (or
|
|
other target branch in the main project).
|
|
* When writing a commit message please follow http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions],
|
|
if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit
|
|
message (where XXXX is the issue number).
|
|
|
|
== How to build it
|
|
|
|
IMPORTANT: You need to have all the necessary Groovy plugins
|
|
installed for your IDE to properly resolve the sources. For example in
|
|
Intellij IDEA having both Eclipse Groovy Compiler Plugin & GMavenPlus Intellij Plugin
|
|
results in properly imported project.
|
|
|
|
=== Project structure
|
|
|
|
Here you can find the Spring Cloud Contract folder structure
|
|
|
|
```
|
|
├── samples
|
|
├── scripts
|
|
├── spring-cloud-contract-dependencies
|
|
├── spring-cloud-contract-spec
|
|
├── spring-cloud-contract-starters
|
|
├── spring-cloud-contract-stub-runner
|
|
├── spring-cloud-contract-tools
|
|
├── spring-cloud-contract-verifier
|
|
├── spring-cloud-contract-wiremock
|
|
└── tests
|
|
```
|
|
|
|
- `samples` - folder contains test samples together with standalone ones used also to build documentation
|
|
- `scripts` - contains scripts to build and test `Spring Cloud Contract` with Maven, Gradle and standalone projects
|
|
- `spring-cloud-contract-dependencies` - contains Spring Cloud Contract BOM
|
|
- `spring-cloud-contract-starters` - contains Spring Cloud Contract Starters
|
|
- `spring-cloud-contract-spec` - contains specification modules (contains concept of a Contract)
|
|
- `spring-cloud-contract-stub-runner` - contains Stub Runner related modules
|
|
- `spring-cloud-contract-tools` - Gradle and Maven plugin for `Spring Cloud Contract Verifier`
|
|
- `spring-cloud-contract-verifier` - core of the `Spring Cloud Contract Verifier` functionality
|
|
- `spring-cloud-contract-wiremock` - all WireMock related functionality
|
|
- `tests` - integration tests for different messaging technologies
|
|
|
|
=== Commands
|
|
|
|
To build the core functionality together with Maven Plugin you can run
|
|
|
|
```
|
|
./mvnw clean install -P integration
|
|
```
|
|
|
|
Calling that function will build core, Maven plugin, Gradle plugin and run end to end tests on the
|
|
standalone samples in proper order (both for Maven and Gradle).
|
|
|
|
To build the Gradle Plugin only
|
|
|
|
```
|
|
cd spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin
|
|
./gradlew clean build
|
|
```
|
|
|
|
=== Helpful scripts
|
|
|
|
We're providing a couple of helpful scripts to build the project.
|
|
|
|
To build the project in parallel (by default uses 4 cores but you can change it)
|
|
|
|
```
|
|
./scripts/parallelBuild.sh
|
|
```
|
|
|
|
and with 8 cores
|
|
|
|
```
|
|
CORES=8 ./scripts/parallelBuild.sh
|
|
```
|
|
|
|
To build the project without any integration tests (by default uses 1 core)
|
|
|
|
```
|
|
./scripts/noIntegration.sh
|
|
```
|
|
|
|
and with 8 cores
|
|
|
|
```
|
|
CORES=8 ./scripts/noIntegration.sh
|
|
```
|
|
|
|
To generate the documentation (both the root one and the maven plugin one)
|
|
|
|
```
|
|
./scripts/generateDocs.sh
|
|
```
|