-
-
-
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 = service.loanApplication(application);
- // then:
- assertThat(loanApplication.getLoanApplicationStatus())
- .isEqualTo(LoanApplicationStatus.LOAN_APPLICATION_REJECTED);
- assertThat(loanApplication.getRejectionReason()).isEqualTo("Amount too high");
-}
+
==== Step by step guide to CDC
+
+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 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/1.0.x/samples/standalone/dsl/http-server[server side code is available here] and https://github.com/spring-cloud/spring-cloud-contract/tree/1.0.x/samples/standalone/dsl/http-client[the client side code here].
+
+TIP: In this case the ownership of the contracts lays on the producer side. It means that physically
+all the contract are present in the producer's repository
+
+===== Technical note
+
+If using the *SNAPSHOT* / *Milestone* / *Release Candidate* versions please add the following section to your
+
+[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
+.Maven
-
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.
+
Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-server/pom.xml[tags=repos,indent=0]
-
ResponseEntity<FraudServiceResponse> response =
- restTemplate.exchange("http://localhost:" + port + "/fraudcheck", HttpMethod.PUT,
- new HttpEntity<>(request, httpHeaders),
- FraudServiceResponse.class);
+
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
+.Gradle
-
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 server side contract. That’s why we need to first clone it.
+
Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-server/build.gradle[tags=deps_repos,indent=0]
-
git clone https://your-git-server.com/server-side.git local-http-server-repo
+
===== 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*
+
+[source,groovy,indent=0]
-
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.
-
-
-
-
-|
-
- |
-
-We’re placing the contract under src/test/resources/contracts/fraud folder. The fraud folder
-is important cause we’ll reference that folder in the producer’s test base class name.
- |
-
-
+
Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java[tags=client_tdd,indent=0]
-
package contracts
+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.
-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')
- }
- }
-}
+*write the missing implementation*
-/*
-From the Consumer perspective, when shooting a request in the integration test:
+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.
-(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.*`
- */
+[source,groovy,indent=0]
+
Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-client/src/main/java/com/example/loan/LoanApplicationService.java[tags=client_call_server,indent=0]
+
+
+
+
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 server side contract. That's why we need to first clone it.
+
+[source,bash,indent=0]
+
+
+
+
+
+
*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.
+
+IMPORTANT: We're placing the contract under `src/test/resources/contracts/fraud` folder. The `fraud` folder
+is important cause we'll reference that folder in the producer's test base class name.
+
+[source,groovy,indent=0]
+
+
+
+
Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.groovy[]
+
+
The Contract is written using a statically typed Groovy DSL. You might be wondering what are those
value(client(…), server(…)) 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
@@ -941,17 +703,7 @@ It’s really important that you understand the map notation to set up contr
-
<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>
+
Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-server/pom.xml[tags=contract_bom,indent=0]
@@ -959,15 +711,7 @@ It’s really important that you understand the map notation to set up contr
-
<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>
+
Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-server/pom.xml[tags=contract_maven_plugin,indent=0]
@@ -1033,17 +777,7 @@ It’s really important that you understand the map notation to set up contr
-
<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>
+
Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-client/pom.xml[tags=contract_bom,indent=0]
@@ -1051,11 +785,7 @@ It’s really important that you understand the map notation to set up contr
-
<dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
- <scope>test</scope>
-</dependency>
+
Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-client/pom.xml[tags=stub_runner,indent=0]
@@ -1063,11 +793,7 @@ It’s really important that you understand the map notation to set up contr
-
@RunWith(SpringRunner.class)
-@SpringBootTest(webEnvironment=WebEnvironment.NONE)
-@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"}, workOffline = true)
-@DirtiesContext
-public class LoanApplicationServiceTests {
+
Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java[tags=autoconfigure_stubrunner,indent=0]
@@ -1110,9 +836,8 @@ public class LoanApplicationServiceTests {
-
@RequestMapping(value = "/fraudcheck", method = PUT)
-public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
-return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);
+Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[tags=server_api,indent=0]
+Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[tags=initial_impl,indent=0]
}
@@ -1130,11 +855,7 @@ git pull https://your-git-server.com/server-side-fork.git contract-change-pr
-
<dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-contract-verifier</artifactId>
- <scope>test</scope>
-</dependency>
+
Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-server/pom.xml[tags=verifier_test_dependencies,indent=0]
@@ -1142,15 +863,7 @@ git pull https://your-git-server.com/server-side-fork.git contract-change-pr
-
<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>
+
Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-server/pom.xml[tags=contract_maven_plugin,indent=0]
-
Now, if you run the ./mvnw clean install you would get sth like this:
+
Results :
+
+
+
Tests in error:
+ ContractVerifierTest.validate_shouldMarkClientAsFraud:32 » IllegalState Parsed…
-
Results :
+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:
-Tests in error:
- ContractVerifierTest.validate_shouldMarkClientAsFraud:32 » IllegalState Parsed...
+[source,java,indent=0]
-
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
+@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:
+ .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.
-
-
-
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)
-public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
-if (amountGreaterThanThreshold(fraudCheck)) {
- return new FraudCheckResult(FraudCheckStatus.FRAUD, AMOUNT_TOO_HIGH);
-}
-return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);
-}
+
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.
+
+[source,java,indent=0]
-
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.
-
-
-
-
Once you’ve finished your work it’s time to deploy your change. First merge the branch
+
Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[tags=server_api,indent=0]
+Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[tags=new_impl,indent=0]
+Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/master/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[tags=initial_impl,indent=0]
+}
-
git checkout master
+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
+
+[source,bash,indent=0]
+
+
+
+
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.
-
-
-
-
-
-
As a developer of the Loan Issuance service (a consumer of the Fraud Detection server):
-
-
-
merge branch to master
+git push origin master
-
git checkout master
-git merge --no-ff contract-change-pr
+
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 server):
+
+*merge branch to master*
+
+[source,bash,indent=0]
-
-
Now you can disable the offline work for Spring Cloud Contract Stub Runner and provide where the repository with your stubs is placed. At this moment the stubs of the server side will be automatically downloaded from Nexus / Artifactory.
-You can switch off the value of the workOffline parameter in your annotation. Below you can see an
-example of achieving the same by changing the properties.
+
git checkout master
+git merge --no-ff contract-change-pr
-
stubrunner:
- ids: 'com.example:http-server-dsl:+:stubs:8080'
- repositoryRoot: http://repo.spring.io/libs-snapshot
+
*work online*
+
+Now you can disable the offline work for Spring Cloud Contract Stub Runner and provide where the repository with your stubs is placed. At this moment the stubs of the server side will be automatically downloaded from Nexus / Artifactory.
+You can switch off the value of the `workOffline` parameter in your annotation. Below you can see an
+example of achieving the same by changing the properties.
+
+[source,yaml,indent=0]
+
Unresolved directive in verifier/introduction.adoc - include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/1.0.x/samples/standalone/dsl/http-client/src/test/resources/application-test-repo.yaml[]
+
+