From 709801c4dee73341493a5f8f270fb71def1af442 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Mon, 29 Jul 2019 14:45:39 +0200 Subject: [PATCH 1/5] Added symbolic link of index.adoc --- docs/src/main/asciidoc/index.adoc | 1 + 1 file changed, 1 insertion(+) create mode 120000 docs/src/main/asciidoc/index.adoc diff --git a/docs/src/main/asciidoc/index.adoc b/docs/src/main/asciidoc/index.adoc new file mode 120000 index 0000000000..3abe7caccd --- /dev/null +++ b/docs/src/main/asciidoc/index.adoc @@ -0,0 +1 @@ +spring-cloud-contract.adoc \ No newline at end of file From 53eede4c3d450bf5ce7ef851693bf90ce80ba974 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 30 Jul 2019 11:37:40 +0200 Subject: [PATCH 2/5] Fixing docs --- docs/src/main/asciidoc/verifier_faq.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/verifier_faq.adoc b/docs/src/main/asciidoc/verifier_faq.adoc index 2c17140c21..f379e662ed 100644 --- a/docs/src/main/asciidoc/verifier_faq.adoc +++ b/docs/src/main/asciidoc/verifier_faq.adoc @@ -1,5 +1,6 @@ +:branch: master :introduction_url: https://raw.githubusercontent.com/spring-cloud/spring-cloud-contract/{branch} -:samples_branch: 2.1.x +:samples_branch: 2.2.x == Spring Cloud Contract FAQ From 70bd589f13ef94ff2561685038d71470cde4e96f Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 30 Jul 2019 11:40:30 +0200 Subject: [PATCH 3/5] Simplified the readme --- README.adoc | 1479 ---------------------------- docs/src/main/asciidoc/README.adoc | 10 - 2 files changed, 1489 deletions(-) diff --git a/README.adoc b/README.adoc index ec3c7e4919..bedf31a989 100644 --- a/README.adoc +++ b/README.adoc @@ -25,1485 +25,6 @@ If you prefer to learn about the project by doing some tutorials, you can check workshops under https://cloud-samples.spring.io/spring-cloud-contract-samples/workshops.html[this link]. -=== Spring Cloud Contract Verifier - -== Spring Cloud Contract Verifier Introduction - -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 4, JUnit 5, TestNG 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. - -=== History - -Before becoming Spring Cloud Contract, this project was called https://github.com/Codearte/accurest[Accurest]. -It was created by https://twitter.com/mgrzejszczak[Marcin Grzejszczak] and https://twitter.com/jkubrynski[Jakub Kubrynski] -from (https://github.com/Codearte[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 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. - -[[spring-cloud-contract-verifier-intro-three-second-tour]] -==== A Three-second Tour - -This very brief tour walks through using Spring Cloud Contract: - -* <> -* <> - -You can find a somewhat longer tour -<>. - -[[spring-cloud-contract-verifier-intro-three-second-tour-producer]] -===== On the Producer Side - -To start working with Spring Cloud Contract, add files with `REST/` messaging contracts -expressed in either Groovy DSL or YAML to the contracts directory, which is set by the -`contractsDslDir` property. By default, it is `$rootDir/src/test/resources/contracts`. - -Then add the Spring Cloud Contract Verifier dependency and plugin to your build file, as -shown in the following example: - -[source,xml,indent=0] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-server/pom.xml[tags=verifier_test_dependencies,indent=0] ----- - -The following listing shows how to add the plugin, which should go in the build/plugins -portion of the file: - -[source,xml,indent=0] ----- - - org.springframework.cloud - spring-cloud-contract-maven-plugin - ${spring-cloud-contract.version} - true - ----- - -Running `./mvnw clean install` automatically generates tests that verify the application -compliance with the added contracts. By default, the tests get generated under -`org.springframework.cloud.contract.verifier.tests.`. - -As the implementation of the functionalities described by the contracts is not yet -present, the tests fail. - -To make them pass, you must add the correct implementation of either handling HTTP -requests or messages. Also, you must add a correct base test class for auto-generated -tests to the project. This class is extended by all the auto-generated tests, and it -should contain all the setup necessary to run them (for example `RestAssuredMockMvc` -controller setup or messaging test setup). - -Once the implementation and the test base class are in place, the tests pass, and both the -application and the stub artifacts are built and installed in the local Maven repository. -The changes can now be merged, and both the application and the stub artifacts may be -published in an online repository. - -[[spring-cloud-contract-verifier-intro-three-second-tour-consumer]] -===== On the Consumer Side - -`Spring Cloud Contract Stub Runner` can be used in the integration tests to get a running -WireMock instance or messaging route that simulates the actual service. - -To do so, add the dependency to `Spring Cloud Contract Stub Runner`, as shown in the -following example: - -[source,xml,indent=0] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-client/pom.xml[tags=stub_runner,indent=0] ----- - -You can get the Producer-side stubs installed in your Maven repository in either of two -ways: - -* By checking out the Producer side repository and adding contracts and generating the stubs -by running the following commands: -+ -[source,bash,indent=0] ----- -$ cd local-http-server-repo -$ ./mvnw clean install -DskipTests ----- -TIP: The tests are being skipped because the Producer-side contract implementation is not -in place yet, so the automatically-generated contract tests fail. -* By getting already-existing producer service stubs from a remote repository. To do so, -pass the stub artifact IDs and artifact repository URL as `Spring Cloud Contract -Stub Runner` properties, as shown in the following example: -+ -[source,yaml,indent=0] ----- -stubrunner: - ids: 'com.example:http-server-dsl:+:stubs:8080' - repositoryRoot: https://repo.spring.io/libs-snapshot ----- - -Now you can annotate your test class with `@AutoConfigureStubRunner`. In the annotation, -provide the `group-id` and `artifact-id` values for `Spring Cloud Contract Stub Runner` to -run the collaborators' stubs for you, as shown in the following example: - -[source,java, indent=0] ----- -@RunWith(SpringRunner.class) -@SpringBootTest(webEnvironment=WebEnvironment.NONE) -@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"}, - stubsMode = StubRunnerProperties.StubsMode.LOCAL) -public class LoanApplicationServiceTests { ----- - -TIP: Use the `REMOTE` `stubsMode` when downloading stubs from an online repository and -`LOCAL` for offline work. - -Now, in your integration test, you can receive stubbed versions of HTTP responses or -messages that are expected to be emitted by the collaborator service. - -[[spring-cloud-contract-verifier-intro-three-minute-tour]] -==== A Three-minute Tour - -This brief tour walks through using Spring Cloud Contract: - -* <> -* <> - -You can find an even more brief tour -<>. - -[[spring-cloud-contract-verifier-intro-three-minute-tour-producer]] -===== On the Producer Side - -To start working with `Spring Cloud Contract`, add files with `REST/` messaging contracts -expressed in either Groovy DSL or YAML to the contracts directory, which is set by the -`contractsDslDir` property. By default, it is `$rootDir/src/test/resources/contracts`. - -For the HTTP stubs, a contract defines what kind of response should be returned for a -given request (taking into account the HTTP methods, URLs, headers, status codes, and so -on). The following example shows how an HTTP stub contract in Groovy DSL: - -[source,groovy,indent=0] ----- -package contracts - -org.springframework.cloud.contract.spec.Contract.make { - request { - method 'PUT' - url '/fraudcheck' - body([ - "client.id": $(regex('[0-9]{10}')), - loanAmount: 99999 - ]) - headers { - contentType('application/json') - } - } - response { - status OK() - body([ - fraudCheckStatus: "FRAUD", - "rejection.reason": "Amount too high" - ]) - headers { - contentType('application/json') - } - } -} ----- - -The same contract expressed in YAML would look like the following example: - -[source,yaml,indent=0] ----- -request: - method: PUT - url: /fraudcheck - body: - "client.id": 1234567890 - loanAmount: 99999 - headers: - Content-Type: application/json - matchers: - body: - - path: $.['client.id'] - type: by_regex - value: "[0-9]{10}" -response: - status: 200 - body: - fraudCheckStatus: "FRAUD" - "rejection.reason": "Amount too high" - headers: - Content-Type: application/json;charset=UTF-8 ----- - -In the case of messaging, you can define: - -* The input and the output messages can be defined (taking into account from and where it -was sent, the message body, and the header). -* The methods that should be called after the message is received. -* The methods that, when called, should trigger a message. - -The following example shows a Camel messaging contract expressed in Groovy DSL: - -[source,groovy] ----- -Unresolved directive in verifier_introduction.adoc - include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_no_output_dsl] ----- - -The following example shows the same contract expressed in YAML: - -[source,yml,indent=0] ----- -Unresolved directive in verifier_introduction.adoc - include::{verifier_core_path}/src/test/resources/yml/contract_message_scenario3.yml[indent=0] ----- - -Then you can add Spring Cloud Contract Verifier dependency and plugin to your build file, -as shown in the following example: - -[source,xml,indent=0] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-server/pom.xml[tags=verifier_test_dependencies,indent=0] ----- - -The following listing shows how to add the plugin, which should go in the build/plugins -portion of the file: - -[source,xml,indent=0] ----- - - org.springframework.cloud - spring-cloud-contract-maven-plugin - ${spring-cloud-contract.version} - true - ----- - -Running `./mvnw clean install` automatically generates tests that verify the application -compliance with the added contracts. By default, the generated tests are under -`org.springframework.cloud.contract.verifier.tests.`. - -The following example shows a sample auto-generated test for an HTTP contract: - -[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"); -} ----- - -The preceding example uses Spring's `MockMvc` to run the tests. This is the default test -mode for HTTP contracts. However, JAX-RS client and explicit HTTP invocations can also be -used. (To do so, change the `testMode` property of the plugin to `JAX-RS` or `EXPLICIT`, -respectively.) - -Since 2.1.0, it is also possible to use `RestAssuredWebTestClient`with Spring's reactive `WebTestClient` -run under the hood. This is particularly recommended while working with Reactive, `Web-Flux`-based applications. -In order to use `WebTestClient` set `testMode` to `WEBTESTCLIENT`. - -Here is an example of a test generated in `WEBTESTCLIENT` test mode: - - [source,java,indent=0] ----- -@Test - public void validate_shouldRejectABeerIfTooYoung() throws Exception { - // given: - WebTestClientRequestSpecification request = given() - .header("Content-Type", "application/json") - .body("{\"age\":10}"); - - // when: - WebTestClientResponse response = given().spec(request) - .post("/check"); - - // then: - assertThat(response.statusCode()).isEqualTo(200); - assertThat(response.header("Content-Type")).matches("application/json.*"); - // and: - DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); - assertThatJson(parsedJson).field("['status']").isEqualTo("NOT_OK"); - } ----- - -Apart from the default JUnit 4, you can instead use JUnit 5, TestNG or Spock tests, by setting the plugin -`testFramework` property to either `JUNIT5`, `TESTNG` or `Spock`. - -TIP: You can now also generate WireMock scenarios based on the contracts, by including an -order number followed by an underscore at the beginning of the contract file names. - -The following example shows an auto-generated test in Spock for a messaging stub contract: - - [source,groovy,indent=0] ----- -given: - ContractVerifierMessage inputMessage = contractVerifierMessaging.create( - \'\'\'{"bookName":"foo"}\'\'\', - ['sample': 'header'] - ) - -when: - contractVerifierMessaging.send(inputMessage, 'jms:delete') - -then: - noExceptionThrown() - bookWasDeleted() ----- - -As the implementation of the functionalities described by the contracts is not yet -present, the tests fail. - -To make them pass, you must add the correct implementation of handling either HTTP -requests or messages. Also, you must add a correct base test class for auto-generated -tests to the project. This class is extended by all the auto-generated tests and should -contain all the setup necessary to run them (for example, `RestAssuredMockMvc` controller -setup or messaging test setup). - -Once the implementation and the test base class are in place, the tests pass, and both the -application and the stub artifacts are built and installed in the local Maven repository. -Information about installing the stubs jar to the local repository appears in the logs, as -shown in the following example: - -[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 ----- - -You can now merge the changes and publish both the application and the stub artifacts -in an online repository. - -*Docker Project* - -In order to enable working with contracts while creating applications in non-JVM -technologies, the `springcloud/spring-cloud-contract` Docker image has been created. It -contains a project that automatically generates tests for HTTP contracts and executes them -in `EXPLICIT` test mode. Then, if the tests pass, it generates Wiremock stubs and, -optionally, publishes them to an artifact manager. In order to use the image, you can -mount the contracts into the `/contracts` directory and set a few environment variables. -// TODO: We should answer the obvious question: Which environment variables? - -[[spring-cloud-contract-verifier-intro-three-minute-tour-consumer]] -===== On the Consumer Side - -`Spring Cloud Contract Stub Runner` can be used in the integration tests to get a running -WireMock instance or messaging route that simulates the actual service. - -To get started, add the dependency to `Spring Cloud Contract Stub Runner`: - -[source,xml,indent=0] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-client/pom.xml[tags=stub_runner,indent=0] ----- - -You can get the Producer-side stubs installed in your Maven repository in either of two -ways: - -* By checking out the Producer side repository and adding contracts and generating the -stubs by running the following commands: -+ -[source,bash,indent=0] ----- -$ cd local-http-server-repo -$ ./mvnw clean install -DskipTests ----- -NOTE: The tests are skipped because the Producer-side contract implementation is not yet -in place, so the automatically-generated contract tests fail. -* Getting already existing producer service stubs from a remote repository. To do so, -pass the stub artifact IDs and artifact repository URl as `Spring Cloud Contract Stub -Runner` properties, as shown in the following example: -+ -[source,yaml,indent=0] ----- -stubrunner: - ids: 'com.example:http-server-dsl:+:stubs:8080' - repositoryRoot: https://repo.spring.io/libs-snapshot ----- - -Now you can annotate your test class with `@AutoConfigureStubRunner`. In the annotation, -provide the `group-id` and `artifact-id` for `Spring Cloud Contract Stub Runner` to run -the collaborators' stubs for you, as shown in the following example: - -[source,java, indent=0] ----- -@RunWith(SpringRunner.class) -@SpringBootTest(webEnvironment=WebEnvironment.NONE) -@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"}, - stubsMode = StubRunnerProperties.StubsMode.LOCAL) -public class LoanApplicationServiceTests { ----- - -TIP: Use the `REMOTE` `stubsMode` when downloading stubs from an online repository and -`LOCAL` for offline work. - -In your integration test, you can receive stubbed versions of HTTP responses or messages -that are expected to be emitted by the collaborator service. You can see entries similar -to the following in the build logs: - -[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}] ----- - -==== 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. - -.Groovy DSL -[source,groovy,indent=0] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.groovy[] ----- - -.YAML -[source,yml,indent=0] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/yml/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.yml[] ----- - -==== 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] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-client/src/main/java/com/example/loan/LoanApplicationService.java[tags=client_call_server,indent=0] ----- - -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] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java[tags=autoconfigure_stubrunner,indent=0] ----- - -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, more or less, 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 ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-server/pom.xml[tags=repos,indent=0] ----- - -[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"] -.Gradle ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-server/build.gradle[tags=deps_repos,indent=0] ----- - -==== 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] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java[tags=client_tdd,indent=0] ----- - -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] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-client/src/main/java/com/example/loan/LoanApplicationService.java[tags=client_call_server,indent=0] ----- - -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. - -.Groovy DSL -[source,groovy,indent=0] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.groovy[] ----- - -.YAML -[source,yml,indent=0] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/yml/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.yml[] ----- - -The YML contract is quite straight-forward. However when you take a look at the Contract -written using a statically typed Groovy DSL - you might wonder what the -`value(client(...), server(...))` parts are. 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. -Consult the <> section 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 -https://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] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-server/pom.xml[tags=contract_bom,indent=0] ----- - -Next, add the `Spring Cloud Contract Verifier` Maven plugin - -[source,xml,indent=0] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-server/pom.xml[tags=contract_maven_plugin,indent=0] ----- - -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] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-client/pom.xml[tags=contract_bom,indent=0] ----- - -Add the dependency to `Spring Cloud Contract Stub Runner`: - -[source,xml,indent=0] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-client/pom.xml[tags=stub_runner,indent=0] ----- - -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 (`StubRunnerProperties.StubsMode.LOCAL`). - -[source,groovy,indent=0] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java[tags=autoconfigure_stubrunner,indent=0] ----- - -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] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/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::{introduction_url}/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[tags=initial_impl,indent=0] -} ----- - -*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] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-server/pom.xml[tags=verifier_test_dependencies,indent=0] ----- - -In the configuration of the Maven plugin, pass the `packageWithBaseClasses` property - -[source,xml,indent=0] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-server/pom.xml[tags=contract_maven_plugin,indent=0] ----- - -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 https://github.com/rest-assured/rest-assured[Rest Assured MVC] to -start the server side `FraudDetectionController`. - -[source,java,indent=0] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudBase.java[] ----- - -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"); -} ----- - -If you used the Groovy DSL, you can see, all the `producer()` parts of the Contract that were present in the -`value(consumer(...), producer(...))` blocks got injected into the test. -In case of using YAML, the same applied for the `matchers` sections of the `response`. - -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] ----- -Unresolved directive in verifier_introduction.adoc - include::{introduction_url}/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::{introduction_url}/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::{introduction_url}/samples/standalone/dsl/http-server/src/main/java/com/example/fraud/FraudDetectionController.java[tags=initial_impl,indent=0] -} ----- - -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 set the value of -`stubsMode` to `REMOTE`. 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: https://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 - -- https://www.slideshare.net/MarcinGrzejszczak/stick-to-the-rules-consumer-driven-contracts-201507-confitura[Slides from Marcin Grzejszczak's talk about Accurest] -- https://toomuchcoding.com/blog/categories/accurest/[Accurest related articles from Marcin Grzejszczak's blog] -- https://toomuchcoding.com/blog/categories/spring-cloud-contract/[Spring Cloud Contract related articles from Marcin Grzejszczak's blog] -- https://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: - -* 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://gitter.im/spring-cloud/spring-cloud-contract[Spring Cloud Contract Gitter] -* 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 https://github.com/tomakehurst/wiremock[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-starter-contract-stub-runner` 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] ----- -Unresolved directive in spring-cloud-wiremock.adoc - include::{doc_samples}/src/test/java/com/example/WiremockForDocsTests.java[tags=wiremock_test1] -Unresolved directive in spring-cloud-wiremock.adoc - include::{doc_samples}/src/test/java/com/example/WiremockForDocsTests.java[tags=wiremock_test2] ----- - -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. Also you can inject a bean of type `WireMockServer` into your test. - -=== 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. - -If you're using Spring Cloud Contract's default stub jars, then your -stubs are stored under `/META-INF/group-id/artifact-id/versions/mappings/` folder. If you want to register all stubs from that location, from all embedded JARs, then it's enough to use the following syntax. - -[source,java,indent=0] ----- -Unresolved directive in spring-cloud-wiremock.adoc - include::{wiremock_tests}/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockFilesApplicationWithUrlResourceTests.java[tags=load_all_stubs] ----- - -=== 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 following example: - -[source,java,indent=0] ----- -Unresolved directive in spring-cloud-wiremock.adoc - include::{doc_samples}/src/test/java/com/example/WiremockForDocsClassRuleTests.java[tags=wiremock_test1] -Unresolved directive in spring-cloud-wiremock.adoc - include::{doc_samples}/src/test/java/com/example/WiremockForDocsClassRuleTests.java[tags=wiremock_test2] ----- - -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. - -To disable the custom `RestTemplateBuilder`, set the `wiremock.rest-template-ssl-enabled` -property to `false`. - -=== 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] ----- -Unresolved directive in spring-cloud-wiremock.adoc - include::{doc_samples}/src/test/java/com/example/WiremockForDocsMockServerApplicationTests.java[tags=wiremock_test] ----- - -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 `https://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 -https://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). - -=== Customization of WireMock configuration - -You can register a bean of `org.springframework.cloud.contract.wiremock.WireMockConfigurationCustomizer` type -in order to customize the WireMock configuration (e.g. add custom transformers). -Example: - -[source,java,indent=0] ----- -Unresolved directive in spring-cloud-wiremock.adoc - include::{wiremock_tests}/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockConfigurationCustomizerTests.java[tags=customizer_1] -// perform your customization here -Unresolved directive in spring-cloud-wiremock.adoc - include::{wiremock_tests}/src/test/java/org/springframework/cloud/contract/wiremock/AutoConfigureWireMockConfigurationCustomizerTests.java[tags=customizer_2] ----- - -=== 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 `WebTestClient` 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 using `MockMvc`: - -[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. The same example with `WebTestClient` (used -for testing Spring WebFlux applications) would look like this: - -[source,java,indent=0] ----- -@RunWith(SpringRunner.class) -@SpringBootTest -@AutoConfigureRestDocs(outputDir = "target/snippets") -@AutoConfigureWebTestClient -public class ApplicationTests { - - @Autowired - private WebTestClient client; - - @Test - public void contextLoads() throws Exception { - client.get().uri("/resource").exchange() - .expectBody(String.class).isEqualTo("Hello World") - .consumeWith(document("resource")); - } -} ----- - -Without any additional configuration, these tests create 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] - -import static org.springframework.cloud.contract.wiremock.restdocs.WireMockRestDocs.verify; - ----- -@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. The `WebTestClient` version of this test -has a similar `verify()` static helper that you insert in the same place. - -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 -<= 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 diff --git a/docs/src/main/asciidoc/README.adoc b/docs/src/main/asciidoc/README.adoc index cf91eb0f74..cde64a13e5 100644 --- a/docs/src/main/asciidoc/README.adoc +++ b/docs/src/main/asciidoc/README.adoc @@ -19,16 +19,6 @@ If you prefer to learn about the project by doing some tutorials, you can check workshops under https://cloud-samples.spring.io/spring-cloud-contract-samples/workshops.html[this link]. -=== Spring Cloud Contract Verifier - -include::verifier_introduction.adoc[] - -include::links.adoc[] - -=== Spring Cloud Contract WireMock - -include::spring-cloud-wiremock.adoc[] - == Documentation You can read more about Spring Cloud Contract Verifier by reading the From 746fe40f803cfa86874c70514590ccf5c9f4cbb5 Mon Sep 17 00:00:00 2001 From: Tim Ysewyn Date: Wed, 31 Jul 2019 18:24:25 +0200 Subject: [PATCH 4/5] Fix consumer accept type --- .../cloud/contract/spec/internal/Request.java | 12 ++++++------ .../cloud/contract/spec/internal/Response.java | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/specs/spring-cloud-contract-spec-java/src/main/java/org/springframework/cloud/contract/spec/internal/Request.java b/specs/spring-cloud-contract-spec-java/src/main/java/org/springframework/cloud/contract/spec/internal/Request.java index db1750c747..6972e5f428 100644 --- a/specs/spring-cloud-contract-spec-java/src/main/java/org/springframework/cloud/contract/spec/internal/Request.java +++ b/specs/spring-cloud-contract-spec-java/src/main/java/org/springframework/cloud/contract/spec/internal/Request.java @@ -670,18 +670,18 @@ public class Request extends Common implements RegexCreatingProperty consumer) { + public void headers(Consumer consumer) { this.headers = new Request.RequestHeaders(); - consumer.accept((RequestHeaders) this.headers); + consumer.accept(this.headers); } /** * Allows to configure HTTP cookies. * @param consumer function to manipulate the cookies */ - public void cookies(Consumer consumer) { + public void cookies(Consumer consumer) { this.cookies = new Request.RequestCookies(); - consumer.accept((RequestCookies) this.cookies); + consumer.accept(this.cookies); } /** @@ -755,7 +755,7 @@ public class Request extends Common implements RegexCreatingProperty consumer) { + public void headers(Consumer consumer) { this.headers = new Response.ResponseHeaders(); - consumer.accept((ResponseHeaders) this.headers); + consumer.accept(this.headers); } /** * Allows to configure HTTP cookies. * @param consumer function to manipulate the URL */ - public void cookies(Consumer consumer) { + public void cookies(Consumer consumer) { this.cookies = new Response.ResponseCookies(); - consumer.accept((ResponseCookies) this.cookies); + consumer.accept(this.cookies); } /** @@ -771,7 +771,7 @@ public class Response extends Common implements RegexCreatingProperty Date: Wed, 31 Jul 2019 23:26:28 +0200 Subject: [PATCH 5/5] Added support for incremental Gradle builds fixes gh-1133 --- docs/pom.xml | 4 + .../verifier/plugin/ContractsCopyTask.groovy | 39 ++++-- .../GenerateClientStubsFromDslTask.groovy | 22 +++- .../plugin/GenerateServerTestsTask.groovy | 42 +++---- .../plugin/PublishStubsToScmTask.groovy | 22 +++- ...ngCloudContractVerifierGradlePlugin.groovy | 116 +++++++++++------- .../verifier/plugin/SampleProjectSpec.groovy | 17 ++- .../scenarioProject/build.gradle | 1 + .../ContractVerifierConfigProperties.java | 31 +++++ 9 files changed, 210 insertions(+), 84 deletions(-) diff --git a/docs/pom.xml b/docs/pom.xml index 99972b5f3f..5eeb1fc5bb 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -35,6 +35,10 @@ docs + + pl.project13.maven + git-commit-id-plugin + org.apache.maven.plugins maven-dependency-plugin diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ContractsCopyTask.groovy b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ContractsCopyTask.groovy index fdc4cfe6f8..373a8a3cd9 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ContractsCopyTask.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ContractsCopyTask.groovy @@ -19,8 +19,10 @@ package org.springframework.cloud.contract.verifier.plugin import groovy.transform.CompileDynamic import groovy.transform.CompileStatic import groovy.transform.PackageScope -import org.gradle.api.internal.ConventionTask +import org.gradle.api.DefaultTask import org.gradle.api.logging.Logger +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.WorkResult @@ -37,22 +39,30 @@ import org.springframework.cloud.contract.verifier.converter.ToYamlConverter */ @PackageScope @CompileStatic -class ContractsCopyTask extends ConventionTask { +class ContractsCopyTask extends DefaultTask { private static final String ORIGINAL_PATH = "original" ContractVerifierExtension extension GradleContractsDownloader downloader + @InputDirectory + File input + + @OutputDirectory + File outputContractsFolder + @TaskAction void copy() { - ContractVerifierConfigProperties props = ExtensionToProperties.fromExtension(getExtension()) - File file = getDownloader().downloadAndUnpackContractsIfRequired(getExtension(), props) - file = contractsSubDirIfPresent(logger, file) + ContractVerifierConfigProperties props = props() + if (logger.isDebugEnabled()) { + logger.debug("Config props [" + props + "]") + } + File file = getInput() String antPattern = "${props.includedRootFolderAntPattern}*.*" String slashSeparatedGroupId = project.group.toString().replace(".", File.separator) String slashSeparatedAntPattern = antPattern.replace(slashSeparatedGroupId, project.group.toString()) - String root = root(props) - File outputContractsFolder = outputContractsFolder(root) + String root = root() + File outputContractsFolder = getOutputContractsFolder() project.logger.info("Downloading and unpacking files from [$file] to [$outputContractsFolder]. The inclusion ant patterns are [${antPattern}] and [${slashSeparatedAntPattern}]") copy(file, antPattern, slashSeparatedAntPattern, props, outputContractsFolder) if (getExtension().isConvertToYaml()) { @@ -61,15 +71,26 @@ class ContractsCopyTask extends ConventionTask { } + private File getInput() { + File file = getDownloader().downloadAndUnpackContractsIfRequired(getExtension(), props()) + return contractsSubDirIfPresent(logger, file) + } + + private ContractVerifierConfigProperties props() { + return ExtensionToProperties.fromExtension(getExtension()) + } + @CompileDynamic - private File outputContractsFolder(String root) { + private File getOutputContractsFolder() { + String root = root() File outputContractsFolder = outputFolder(root, "contracts") ext.contractsDslDir = outputContractsFolder return outputContractsFolder } @CompileDynamic - private String root(ContractVerifierConfigProperties props) { + private String root() { + ContractVerifierConfigProperties props = props() String root = OutputFolderBuilder.buildRootPath(project) ext.contractVerifierConfigProperties = props return root diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GenerateClientStubsFromDslTask.groovy b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GenerateClientStubsFromDslTask.groovy index 23f72dbc99..a9b64ded57 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GenerateClientStubsFromDslTask.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GenerateClientStubsFromDslTask.groovy @@ -18,8 +18,10 @@ package org.springframework.cloud.contract.verifier.plugin import groovy.transform.CompileDynamic import groovy.transform.CompileStatic +import org.gradle.api.DefaultTask import org.gradle.api.Task -import org.gradle.api.internal.ConventionTask +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties @@ -33,8 +35,12 @@ import org.springframework.cloud.contract.verifier.converter.RecursiveFilesConve * @since 2.0.0 */ @CompileStatic -class GenerateClientStubsFromDslTask extends ConventionTask { +class GenerateClientStubsFromDslTask extends DefaultTask { + @InputDirectory + File contractsDslDir + + @OutputDirectory File stubsOutputDir ContractVerifierExtension configProperties @@ -45,9 +51,8 @@ class GenerateClientStubsFromDslTask extends ConventionTask { logger.info("Stubs output dir [${getStubsOutputDir()}") Task copyContractsTask = project.getTasksByName(SpringCloudContractVerifierGradlePlugin.COPY_CONTRACTS_TASK_NAME, false).first() ContractVerifierConfigProperties props = props(copyContractsTask) - File contractsDslDir = contractsDslDir(copyContractsTask, props) logger.info("Spring Cloud Contract Verifier Plugin: Invoking DSL to client stubs conversion") - props.contractsDslDir = contractsDslDir + props.contractsDslDir = getContractsDslDir() props.includedContracts = ".*" File outMappingsDir = OutputFolderBuilder.outputMappingsDir(project, getStubsOutputDir()) logger.info("Contracts dir is [${contractsDslDir}] output stubs dir is [${outMappingsDir}]") @@ -55,6 +60,15 @@ class GenerateClientStubsFromDslTask extends ConventionTask { converter.processFiles() } + private File getContractsDslDir() { + Task copyContractsTask = project.getTasksByName(SpringCloudContractVerifierGradlePlugin.COPY_CONTRACTS_TASK_NAME, false).first() + ContractVerifierConfigProperties props = props(copyContractsTask) + if (logger.isDebugEnabled()) { + logger.debug("Config props [" + props + "]") + } + return contractsDslDir(copyContractsTask, props) + } + @CompileDynamic private ContractVerifierConfigProperties props(Task task) { try { diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GenerateServerTestsTask.groovy b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GenerateServerTestsTask.groovy index ff0f13633b..c5af500337 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GenerateServerTestsTask.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/GenerateServerTestsTask.groovy @@ -18,16 +18,16 @@ package org.springframework.cloud.contract.verifier.plugin import groovy.transform.CompileDynamic import groovy.transform.CompileStatic +import org.gradle.api.DefaultTask import org.gradle.api.GradleException import org.gradle.api.Task -import org.gradle.api.internal.ConventionTask +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction -import org.springframework.cloud.contract.spec.Contract import org.springframework.cloud.contract.spec.ContractVerifierException import org.springframework.cloud.contract.verifier.TestGenerator import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties -import org.springframework.cloud.contract.verifier.config.TestFramework import static org.springframework.cloud.contract.verifier.plugin.SpringCloudContractVerifierGradlePlugin.COPY_CONTRACTS_TASK_NAME @@ -37,20 +37,22 @@ import static org.springframework.cloud.contract.verifier.plugin.SpringCloudCont * @since 1.0.0 */ @CompileStatic -class GenerateServerTestsTask extends ConventionTask { +class GenerateServerTestsTask extends DefaultTask { + @InputDirectory + File contractsDslDir + + @OutputDirectory File generatedTestSourcesDir - //TODO: How to deal with @Input*, @Output* and that domain object? ContractVerifierExtension configProperties GradleContractsDownloader downloader @TaskAction void generate() { logger.info("Generated test sources dir [${getGeneratedTestSourcesDir()}]") - Task copyContractsTask = project.getTasksByName(COPY_CONTRACTS_TASK_NAME, false).first() - ContractVerifierConfigProperties props = props(copyContractsTask) - File contractsDslDir = contractsDslDir(copyContractsTask, props) + ContractVerifierConfigProperties props = getProps() + File contractsDslDir = getContractsDslDir() if (getConfigProperties().getContractDependency()) { project.logger.debug("Updating the stubs locations for the case where we have a JAR with contracts") props.contractsDslDir = contractsDslDir @@ -59,9 +61,6 @@ class GenerateServerTestsTask extends ConventionTask { project.logger.info("Spring Cloud Contract Verifier Plugin: Invoking test sources generation") project.logger.info("Contracts are unpacked to [${contractsDslDir}]") project.logger.info("Included contracts are [${props.includedContracts}]") - def sourceSetType = getConfigProperties().getTestFramework() == TestFramework.SPOCK ? - "groovy" : "java" - applySourceSets(sourceSetType) try { props = props ?: ExtensionToProperties.fromExtension(getConfigProperties()) props.contractsDslDir = contractsDslDir @@ -74,18 +73,9 @@ class GenerateServerTestsTask extends ConventionTask { } } - @CompileDynamic - private void applySourceSets(sourceSetType) { - project.sourceSets.test."${sourceSetType}" { - project.logger. - info("Registering ${getConfigProperties().generatedTestSourcesDir} as test source directory") - srcDir getConfigProperties().getGeneratedTestSourcesDir() - } - project.sourceSets.test.resources { - project.logger. - info("Registering ${getConfigProperties().generatedTestResourcesDir} as test resource directory") - srcDir getConfigProperties().getGeneratedTestResourcesDir() - } + private ContractVerifierConfigProperties getProps() { + Task copyContractsTask = project.getTasksByName(COPY_CONTRACTS_TASK_NAME, false).first() + return props(copyContractsTask) } @CompileDynamic @@ -101,6 +91,12 @@ class GenerateServerTestsTask extends ConventionTask { } } + private File getContractsDslDir() { + Task copyContractsTask = project.getTasksByName(COPY_CONTRACTS_TASK_NAME, false).first() + ContractVerifierConfigProperties props = props(copyContractsTask) + return contractsDslDir(copyContractsTask, props) + } + @CompileDynamic private File contractsDslDir(Task task, ContractVerifierConfigProperties props) { try { diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/PublishStubsToScmTask.groovy b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/PublishStubsToScmTask.groovy index 28d8308d41..f3b1abd8db 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/PublishStubsToScmTask.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/PublishStubsToScmTask.groovy @@ -17,7 +17,8 @@ package org.springframework.cloud.contract.verifier.plugin import groovy.transform.CompileStatic -import org.gradle.api.internal.ConventionTask +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction import org.springframework.cloud.contract.stubrunner.ContractProjectUpdater @@ -33,12 +34,17 @@ import org.springframework.cloud.contract.stubrunner.StubRunnerOptions * @since 2.0.0 */ @CompileStatic -class PublishStubsToScmTask extends ConventionTask { - File stubsOutputDir - ContractVerifierExtension configProperties - GradleContractsDownloader downloader +class PublishStubsToScmTask extends DefaultTask { + private final ExtensionHolderSpec closureHolder = new ClosureHolder() + @OutputDirectory + File stubsOutputDir + + ContractVerifierExtension configProperties + + GradleContractsDownloader downloader + @TaskAction void publishStubsToScm() { ContractVerifierExtension clonedExtension = modifyExtension() @@ -67,6 +73,12 @@ class PublishStubsToScmTask extends ConventionTask { return true } + /** + * This method can be used to perform additional customization of + * the {@link ContractVerifierExtension}. + * + * @param closure to customize the {@link ContractVerifierExtension} + */ void customize(@DelegatesTo(ContractVerifierExtension) Closure closure) { project.logger.debug("Storing the extension closure") this.closureHolder.extensionClosure = closure diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/SpringCloudContractVerifierGradlePlugin.groovy b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/SpringCloudContractVerifierGradlePlugin.groovy index 61c5e40d29..355f9309f5 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/SpringCloudContractVerifierGradlePlugin.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/SpringCloudContractVerifierGradlePlugin.groovy @@ -25,6 +25,9 @@ import org.gradle.api.plugins.GroovyPlugin import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.publish.maven.plugins.MavenPublishPlugin import org.gradle.jvm.tasks.Jar + +import org.springframework.cloud.contract.verifier.config.TestFramework + /** * Gradle plugin for Spring Cloud Contract Verifier that from the DSL contract can *
    @@ -63,9 +66,30 @@ class SpringCloudContractVerifierGradlePlugin implements Plugin { Task copyContracts = createAndConfigureCopyContractsTask(stubsJar, downloader, extension) createAndConfigureMavenPublishPlugin(stubsJar, extension) createGenerateTestsTask(extension, copyContracts, downloader) - createAndConfigureGenerateClientStubs(extension, copyContracts) + createAndConfigureGenerateClientStubs(extension, copyContracts, downloader) createAndConfigurePublishStubsToScmTask(extension, downloader) addIdeaTestSources(project, extension) + applyDefaultSourceSets(extension) + } + + private void applyDefaultSourceSets(ContractVerifierExtension extension) { + def sourceSetType = extension.getTestFramework() == TestFramework.SPOCK ? + "groovy" : "java" + applySourceSets(extension, sourceSetType) + } + + @CompileDynamic + private void applySourceSets(ContractVerifierExtension extension, sourceSetType) { + project.sourceSets.test."${sourceSetType}" { + project.logger. + info("Registering ${extension.generatedTestSourcesDir} as test source directory") + srcDir extension.getGeneratedTestSourcesDir() + } + project.sourceSets.test.resources { + project.logger. + info("Registering ${extension.generatedTestResourcesDir} as test resource directory") + srcDir extension.getGeneratedTestResourcesDir() + } } @CompileDynamic @@ -108,49 +132,46 @@ class SpringCloudContractVerifierGradlePlugin implements Plugin { @CompileDynamic private void createGenerateTestsTask(ContractVerifierExtension extension, Task copyContracts, GradleContractsDownloader gradleContractsDownloader) { - Task task = project.tasks.create(GENERATE_SERVER_TESTS_TASK_NAME, GenerateServerTestsTask) + GenerateServerTestsTask task = project.tasks.create(GENERATE_SERVER_TESTS_TASK_NAME, GenerateServerTestsTask) task.description = "Generate server tests from the contracts" task.group = GROUP_NAME - task.conventionMapping.with { - downloader = { gradleContractsDownloader } - generatedTestSourcesDir = { extension.generatedTestSourcesDir } - configProperties = { extension } + task.with { + downloader = gradleContractsDownloader + generatedTestSourcesDir = extension.generatedTestSourcesDir + configProperties = extension } task.enabled = !project.gradle.startParameter.excludedTaskNames.contains("test") task.dependsOn copyContracts project.tasks.findByName("compileTestJava").dependsOn(task) } - @CompileDynamic private void createAndConfigurePublishStubsToScmTask(ContractVerifierExtension extension, GradleContractsDownloader gradleContractsDownloader) { - Task task = project.tasks.create(PUBLISH_STUBS_TO_SCM_TASK_NAME, PublishStubsToScmTask) + PublishStubsToScmTask task = project.tasks.create(PUBLISH_STUBS_TO_SCM_TASK_NAME, PublishStubsToScmTask) task.description = "The generated stubs get committed to the SCM repo and pushed to origin" task.group = GROUP_NAME - task.conventionMapping.with { - downloader = { gradleContractsDownloader } - configProperties = { extension } - stubsOutputDir = { extension.stubsOutputDir } + task.with { + downloader = gradleContractsDownloader + configProperties = extension + stubsOutputDir = extension.stubsOutputDir } task.dependsOn DSL_TO_CLIENT_TASK_NAME } - @CompileDynamic private Task createAndConfigureGenerateClientStubs(ContractVerifierExtension extension, - Task copyContracts) { - Task task = project.tasks.create(DSL_TO_CLIENT_TASK_NAME, GenerateClientStubsFromDslTask) + Task copyContracts, GradleContractsDownloader gradleContractsDownloader) { + GenerateClientStubsFromDslTask task = project.tasks.create(DSL_TO_CLIENT_TASK_NAME, GenerateClientStubsFromDslTask) task.description = "Generate client stubs from the contracts" task.group = GROUP_NAME - task.conventionMapping.with { - downloader = { gradleContractsDownloader } - stubsOutputDir = { extension.stubsOutputDir } - configProperties = { extension } + task.with { + downloader = gradleContractsDownloader + stubsOutputDir = extension.stubsOutputDir + configProperties = extension } task.dependsOn copyContracts return task } - @CompileDynamic private Task createAndConfigureStubsJarTasks(ContractVerifierExtension extension) { Task task = stubsTask() if (task) { @@ -158,21 +179,31 @@ class SpringCloudContractVerifierGradlePlugin implements Plugin { return task } else { - task = project.tasks.create(type: Jar, name: VERIFIER_STUBS_JAR_TASK_NAME, - dependsOn: DSL_TO_CLIENT_TASK_NAME) { - baseName = project.name - classifier = extension.stubsSuffix - from { extension.stubsOutputDir ?: project.file("${project.buildDir}/stubs") } - } + task = createStubsJarTask(extension) task.description = "Creates the stubs JAR task" task.group = GROUP_NAME - project.artifacts { - archives task - } + setArchives(task) return task } } + @CompileDynamic + private Task createStubsJarTask(ContractVerifierExtension extension) { + return project.tasks.create(type: Jar, name: VERIFIER_STUBS_JAR_TASK_NAME, + dependsOn: DSL_TO_CLIENT_TASK_NAME) { + getArchiveBaseName().set(project.name) + getArchiveClassifier().set(extension.stubsSuffix) + from { extension.stubsOutputDir ?: project.file("${project.buildDir}/stubs") } + } + } + + @CompileDynamic + private void setArchives(Task task) { + project.artifacts { + archives task + } + } + private Task stubsTask() { try { return project.tasks.getByName(VERIFIER_STUBS_JAR_TASK_NAME) @@ -182,22 +213,20 @@ class SpringCloudContractVerifierGradlePlugin implements Plugin { } } - @CompileDynamic private Task createAndConfigureCopyContractsTask(Task stubs, GradleContractsDownloader gradleContractsDownloader, ContractVerifierExtension contractVerifierExtension) { - Task task = project.tasks.create(COPY_CONTRACTS_TASK_NAME, ContractsCopyTask) + ContractsCopyTask task = project.tasks.create(COPY_CONTRACTS_TASK_NAME, ContractsCopyTask) task.description = "Copies contracts to the output folder" task.group = GROUP_NAME - task.conventionMapping.with { - downloader = { gradleContractsDownloader } - extension = { contractVerifierExtension } + task.with { + downloader = gradleContractsDownloader + extension = contractVerifierExtension } stubs.dependsOn task return task } - @CompileDynamic private void createAndConfigureMavenPublishPlugin(Task stubsTask, ContractVerifierExtension extension) { if (!classIsOnClasspath("org.gradle.api.publish.maven.plugins.MavenPublishPlugin")) { project.logger.debug("Maven Publish Plugin is not present - won't add default publication") @@ -213,12 +242,7 @@ class SpringCloudContractVerifierGradlePlugin implements Plugin { def publishingExtension = project.extensions.findByName('publishing') if (!hasPublication(publishingExtension)) { project.logger.debug("Spring Cloud Contract Verifier Plugin: Stubs publication is not present - will create one") - publishingExtension.publications { - stubs(MavenPublication) { - artifactId "${project.name}" - artifact stubsTask - } - } + setPublications(publishingExtension, stubsTask) } else { project.logger.info("Spring Cloud Contract Verifier Plugin: Stubs publication was present - won't create a new one. Remember about passing stubs as artifact") @@ -227,6 +251,16 @@ class SpringCloudContractVerifierGradlePlugin implements Plugin { } } + @CompileDynamic + private void setPublications(def publishingExtension, Task stubsTask) { + publishingExtension.publications { + stubs(MavenPublication) { + artifactId "${project.name}" + artifact stubsTask + } + } + } + @CompileDynamic private boolean hasPublication(def publishingExtension) { try { diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/SampleProjectSpec.groovy b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/SampleProjectSpec.groovy index 552fe5173f..d59cb7b6e8 100755 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/SampleProjectSpec.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/SampleProjectSpec.groovy @@ -19,6 +19,8 @@ package org.springframework.cloud.contract.verifier.plugin import org.junit.Ignore import spock.lang.Stepwise +import static org.gradle.testkit.runner.TaskOutcome.UP_TO_DATE + @Stepwise @Ignore class SampleProjectSpec extends ContractVerifierIntegrationSpec { @@ -32,22 +34,33 @@ class SampleProjectSpec extends ContractVerifierIntegrationSpec { def "should pass basic flow for Spock"() { given: assert fileExists('build.gradle') - expect: + when: String[] args = ["check", "publishToMavenLocal", "--debug"] as String[] if (WORK_OFFLINE) { args << "--offline" } runTasksSuccessfully(args) + then: jarContainsContractVerifierContracts('fraudDetectionService/build/libs') + when: "running generation without change inputs" + def secondExecutionResult = runTasksSuccessfully() + then: "tasks should be up-to-date" + validateTasksOutcome(secondExecutionResult, UP_TO_DATE, 'generateClientStubs', 'generateContractTests', 'copyContracts') + } def "should pass basic flow for JUnit"() { given: switchToJunitTestFramework() assert fileExists('build.gradle') - expect: + when: runTasksSuccessfully(checkAndPublishToMavenLocal()) + then: jarContainsContractVerifierContracts('fraudDetectionService/build/libs') + when: "running generation without change inputs" + def secondExecutionResult = runTasksSuccessfully() + then: "tasks should be up-to-date" + validateTasksOutcome(secondExecutionResult, UP_TO_DATE, 'generateClientStubs', 'generateContractTests', 'copyContracts') } } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle index 0b1c78981f..9d6e180da1 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle @@ -43,6 +43,7 @@ subprojects { sourceCompatibility = 1.8 targetCompatibility = 1.8 + version = "1.0.0" repositories { mavenCentral() diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/config/ContractVerifierConfigProperties.java b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/config/ContractVerifierConfigProperties.java index 3693d49a62..e05edd65be 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/config/ContractVerifierConfigProperties.java +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/config/ContractVerifierConfigProperties.java @@ -18,8 +18,10 @@ package org.springframework.cloud.contract.verifier.config; import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.StringJoiner; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -366,4 +368,33 @@ public class ContractVerifierConfigProperties { this.excludeBuildFolders = excludeBuildFolders; } + @Override + public String toString() { + return new StringJoiner(", ", + ContractVerifierConfigProperties.class.getSimpleName() + "[", "]") + .add("targetFramework=" + targetFramework) + .add("testFramework=" + testFramework).add("testMode=" + testMode) + .add("basePackageForTests='" + basePackageForTests + "'") + .add("baseClassForTests='" + baseClassForTests + "'") + .add("nameSuffixForTests='" + nameSuffixForTests + "'") + .add("ruleClassForTests='" + ruleClassForTests + "'") + .add("excludedFiles=" + excludedFiles) + .add("includedFiles=" + includedFiles) + .add("ignoredFiles=" + ignoredFiles) + .add("imports=" + Arrays.toString(imports)) + .add("staticImports=" + Arrays.toString(staticImports)) + .add("contractsDslDir=" + contractsDslDir) + .add("generatedTestSourcesDir=" + generatedTestSourcesDir) + .add("generatedTestResourcesDir=" + generatedTestResourcesDir) + .add("stubsOutputDir=" + stubsOutputDir) + .add("stubsSuffix='" + stubsSuffix + "'") + .add("assertJsonSize=" + assertJsonSize) + .add("includedContracts='" + includedContracts + "'") + .add("includedRootFolderAntPattern='" + + includedRootFolderAntPattern + "'") + .add("packageWithBaseClasses='" + packageWithBaseClasses + "'") + .add("baseClassMappings=" + baseClassMappings) + .add("excludeBuildFolders=" + excludeBuildFolders).toString(); + } + }