From 8c5f2f556760484c05155fa548d5b569e8bb003f Mon Sep 17 00:00:00 2001 From: Tim Ysewyn Date: Wed, 9 May 2018 07:57:13 +0200 Subject: [PATCH] Updated documentation for generating Spring REST Docs snippets (#648) * Updated documentation with examples how to generate Spring REST Docs snippets from the generated tests. fixes gh-613 --- .../spring-cloud-contract-verifier.adoc | 1 + docs/src/main/asciidoc/verifier_contract.adoc | 34 ++++++++++ .../restdocs/http-server/build.gradle | 16 ++++- .../standalone/restdocs/http-server/pom.xml | 38 +++++++++-- .../fraud/FraudBaseWithStandaloneSetup.java | 31 +++++++++ .../fraud/FraudBaseWithWebAppSetup.java | 47 +++++++++++++ .../standalone/shouldMarkClientAsFraud.groovy | 66 +++++++++++++++++++ .../webapp/shouldMarkClientAsNotFraud.groovy | 29 ++++++++ 8 files changed, 256 insertions(+), 6 deletions(-) create mode 100644 samples/standalone/restdocs/http-server/src/test/java/com/example/fraud/FraudBaseWithStandaloneSetup.java create mode 100644 samples/standalone/restdocs/http-server/src/test/java/com/example/fraud/FraudBaseWithWebAppSetup.java create mode 100644 samples/standalone/restdocs/http-server/src/test/resources/contracts/standalone/shouldMarkClientAsFraud.groovy create mode 100644 samples/standalone/restdocs/http-server/src/test/resources/contracts/webapp/shouldMarkClientAsNotFraud.groovy diff --git a/docs/src/main/asciidoc/spring-cloud-contract-verifier.adoc b/docs/src/main/asciidoc/spring-cloud-contract-verifier.adoc index ce6cfcdaa7..21183b4e5f 100644 --- a/docs/src/main/asciidoc/spring-cloud-contract-verifier.adoc +++ b/docs/src/main/asciidoc/spring-cloud-contract-verifier.adoc @@ -9,6 +9,7 @@ :standalone_samples_path: {samples_path}/standalone/dsl :standalone_messaging_samples_path: {samples_path}/standalone/messaging :standalone_pact_path: {samples_path}/standalone/pact +:standalone_restdocs_path: {samples_path}/standalone/restdocs :tests_path: {core_path}/tests :samples_url: https://raw.githubusercontent.com/spring-cloud-samples/spring-cloud-contract-samples/master :introduction_url: ${core_path}/../../ diff --git a/docs/src/main/asciidoc/verifier_contract.adoc b/docs/src/main/asciidoc/verifier_contract.adoc index 335e52d1bf..08337c667b 100644 --- a/docs/src/main/asciidoc/verifier_contract.adoc +++ b/docs/src/main/asciidoc/verifier_contract.adoc @@ -1270,6 +1270,40 @@ case, the contract had an index of `1` in the list of contracts in the file). TIP: As you can see, it iss much better if you name your contracts because doing so makes your tests far more meaningful. +=== Generating Spring REST Docs snippets from the contracts + +When you want to include the requests and responses of your API using Spring REST Docs, +you only need to make some minor changes to your setup if you are using MockMvc and RestAssuredMockMvc. +Simply include the following dependencies if you haven't already. + +[source,xml,indent=0] +.Maven +---- +include::{standalone_restdocs_path}/http-server/pom.xml[tags=dependencies,indent=0] +---- + +[source,groovy,indent=0] +.Gradle +---- +include::{standalone_restdocs_path}/http-server/build.gradle[tags=dependencies,indent=0] +---- + +Next you need to make some changes to your base class like the following example. + +[source,java,indent=0] +---- +include::{standalone_restdocs_path}/http-server/src/test/java/com/example/fraud/FraudBaseWithWebAppSetup.java[tags=base_class,indent=0] +---- + +In case you are using the standalone setup, you can set up RestAssuredMockMvc like this: + +[source,java,indent=0] +---- +include::{standalone_restdocs_path}/http-server/src/test/java/com/example/fraud/FraudBaseWithStandaloneSetup.java[tags=base_class,indent=0] +---- + +TIP: You don't need to specify the output directory for the generated snippets since version 1.2.0.RELEASE of Spring REST Docs. + == Customization IMPORTANT: This section is valid only for Groovy DSL diff --git a/samples/standalone/restdocs/http-server/build.gradle b/samples/standalone/restdocs/http-server/build.gradle index 4144159e31..5797f5c1f7 100644 --- a/samples/standalone/restdocs/http-server/build.gradle +++ b/samples/standalone/restdocs/http-server/build.gradle @@ -11,6 +11,7 @@ buildscript { } dependencies { classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.1.RELEASE" + classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${project.findProperty('verifierVersion') ?: verifierVersion}" } } @@ -30,6 +31,7 @@ apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' apply plugin: 'maven-publish' apply plugin: 'maven' +apply plugin: 'spring-cloud-contract' dependencyManagement { imports { @@ -44,8 +46,11 @@ dependencies { compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc' testCompile 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner' + // tag::dependencies[] + testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier' + testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc' + // end::dependencies[] } test { @@ -55,6 +60,15 @@ test { } } +contracts { + baseClassMappings { + baseClassMapping('.*standalone.*', 'com.example.fraud.FraudBaseWithStandaloneSetup') + baseClassMapping('.*webapp.*', 'com.example.fraud.FraudBaseWithWebAppSetup') + } +} + +generateClientStubs.enabled = false + task wrapper(type: Wrapper) { gradleVersion = '4.0.2' } diff --git a/samples/standalone/restdocs/http-server/pom.xml b/samples/standalone/restdocs/http-server/pom.xml index 4e08c64a67..db18d0aa9d 100644 --- a/samples/standalone/restdocs/http-server/pom.xml +++ b/samples/standalone/restdocs/http-server/pom.xml @@ -21,6 +21,7 @@ UTF-8 1.8 2.0.0.BUILD-SNAPSHOT + true @@ -43,16 +44,23 @@ test - - org.springframework.restdocs - spring-restdocs-mockmvc - true - org.springframework.cloud spring-cloud-starter-contract-stub-runner test + + + org.springframework.cloud + spring-cloud-starter-contract-verifier + test + + + org.springframework.restdocs + spring-restdocs-mockmvc + true + + @@ -113,6 +121,26 @@ + + + org.springframework.cloud + spring-cloud-contract-maven-plugin + ${spring-cloud-contract.version} + true + + + + .*standalone.* + com.example.fraud.FraudBaseWithStandaloneSetup + + + .*webapp.* + com.example.fraud.FraudBaseWithWebAppSetup + + + + + diff --git a/samples/standalone/restdocs/http-server/src/test/java/com/example/fraud/FraudBaseWithStandaloneSetup.java b/samples/standalone/restdocs/http-server/src/test/java/com/example/fraud/FraudBaseWithStandaloneSetup.java new file mode 100644 index 0000000000..4484a09e8a --- /dev/null +++ b/samples/standalone/restdocs/http-server/src/test/java/com/example/fraud/FraudBaseWithStandaloneSetup.java @@ -0,0 +1,31 @@ +// tag::base_class[] +package com.example.fraud; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.TestName; +import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + +public abstract class FraudBaseWithStandaloneSetup { + + private static final String OUTPUT = "target/generated-snippets"; + + @Rule + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(OUTPUT); + + @Rule public TestName testName = new TestName(); + + @Before + public void setup() { + RestAssuredMockMvc.standaloneSetup(MockMvcBuilders.standaloneSetup(new FraudDetectionController()) + .apply(documentationConfiguration(this.restDocumentation)) + .alwaysDo(document(getClass().getSimpleName() + "_" + testName.getMethodName()))); + } + +} +// end::base_class[] \ No newline at end of file diff --git a/samples/standalone/restdocs/http-server/src/test/java/com/example/fraud/FraudBaseWithWebAppSetup.java b/samples/standalone/restdocs/http-server/src/test/java/com/example/fraud/FraudBaseWithWebAppSetup.java new file mode 100644 index 0000000000..ffcf09ddad --- /dev/null +++ b/samples/standalone/restdocs/http-server/src/test/java/com/example/fraud/FraudBaseWithWebAppSetup.java @@ -0,0 +1,47 @@ +// tag::base_class[] +package com.example.fraud; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Application.class) +public abstract class FraudBaseWithWebAppSetup { + + private static final String OUTPUT = "target/generated-snippets"; + + @Rule + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(OUTPUT); + + @Rule public TestName testName = new TestName(); + + @Autowired + private WebApplicationContext context; + + @Before + public void setup() { + RestAssuredMockMvc.mockMvc(MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)) + .alwaysDo(document(getClass().getSimpleName() + "_" + testName.getMethodName())) + .build()); + } + + protected void assertThatRejectionReasonIsNull(Object rejectionReason) { + assert rejectionReason == null; + } +} +// end::base_class[] \ No newline at end of file diff --git a/samples/standalone/restdocs/http-server/src/test/resources/contracts/standalone/shouldMarkClientAsFraud.groovy b/samples/standalone/restdocs/http-server/src/test/resources/contracts/standalone/shouldMarkClientAsFraud.groovy new file mode 100644 index 0000000000..10fec1a686 --- /dev/null +++ b/samples/standalone/restdocs/http-server/src/test/resources/contracts/standalone/shouldMarkClientAsFraud.groovy @@ -0,0 +1,66 @@ +package contracts.standalone + +org.springframework.cloud.contract.spec.Contract.make { + request { // (1) + method 'PUT' // (2) + url '/fraudcheck' // (3) + body([ // (4) + clientId: $(c(regex('[0-9]{10}')), p("8532032713")), + loanAmount: 99999 + ]) + headers { // (5) + contentType('application/vnd.fraud.v1+json') + } + } + response { // (6) + status OK() // (7) + body([ // (8) + fraudCheckStatus: "FRAUD", + rejectionReason: "Amount too high" + ]) + headers { // (9) + contentType('application/vnd.fraud.v1+json') + } + } +} + +/* +Since we don't want to force on the user to hardcode values of fields that are dynamic +(timestamps, database ids etc.), one can parametrize those entries. If you wrap your field's + value in a `$(...)` or `value(...)` and provide a dynamic value of a field then + the concrete value will be generated for you. If you want to be really explicit about + which side gets which value you can do that by using the `value(consumer(...), producer(...))` notation. + That way what's present in the `consumer` section will end up in the produced stub. What's + there in the `producer` will end up in the autogenerated test. If you provide only the + regular expression side without the concrete value then Spring Cloud Contract will generate one for you. + +From the Consumer perspective, when shooting a request in the integration test: + +(1) - If the consumer sends a request +(2) - With the "PUT" method +(3) - to the URL "/fraudcheck" +(4) - with the JSON body that + * has a field `clientId` that matches a regular expression `[0-9]{10}` + * has a field `loanAmount` that is equal to `99999` +(5) - with header `Content-Type` equal to `application/vnd.fraud.v1+json` +(6) - then the response will be sent with +(7) - status equal `200` +(8) - and JSON body equal to + { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +(9) - with header `Content-Type` equal to `application/vnd.fraud.v1+json` + +From the Producer perspective, in the autogenerated producer-side test: + +(1) - A request will be sent to the producer +(2) - With the "PUT" method +(3) - to the URL "/fraudcheck" +(4) - with the JSON body that + * has a field `clientId` that will have a generated value that matches a regular expression `[0-9]{10}` + * has a field `loanAmount` that is equal to `99999` +(5) - with header `Content-Type` equal to `application/vnd.fraud.v1+json` +(6) - then the test will assert if the response has been sent with +(7) - status equal `200` +(8) - and JSON body equal to + { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +(9) - with header `Content-Type` matching `application/vnd.fraud.v1+json.*` + */ \ No newline at end of file diff --git a/samples/standalone/restdocs/http-server/src/test/resources/contracts/webapp/shouldMarkClientAsNotFraud.groovy b/samples/standalone/restdocs/http-server/src/test/resources/contracts/webapp/shouldMarkClientAsNotFraud.groovy new file mode 100644 index 0000000000..e2ad1731c8 --- /dev/null +++ b/samples/standalone/restdocs/http-server/src/test/resources/contracts/webapp/shouldMarkClientAsNotFraud.groovy @@ -0,0 +1,29 @@ +package contracts.webapp + +org.springframework.cloud.contract.spec.Contract.make { + request { + method 'PUT' + url '/fraudcheck' + body(""" + { + "clientId":"${value(consumer(regex('[0-9]{10}')), producer('1234567890'))}", + "loanAmount":123.123 + } + """ + ) + headers { + contentType("application/vnd.fraud.v1+json") + } + + } + response { + status OK() + body( + fraudCheckStatus: "OK", + rejectionReason: $(consumer(null), producer(execute('assertThatRejectionReasonIsNull($it)'))) + ) + headers { + contentType("application/vnd.fraud.v1+json") + } + } +}