Add support for using WebFlux's WebTestClient to document an API

Closes gh-384
This commit is contained in:
Andy Wilkinson
2017-10-30 09:40:24 +00:00
parent 1cd74a5c1d
commit cfb1fbc85d
52 changed files with 2948 additions and 58 deletions

View File

@@ -16,7 +16,6 @@ allprojects {
group = 'org.springframework.restdocs'
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
}
}
@@ -35,7 +34,7 @@ sonarqube {
}
ext {
springVersion = '5.0.0.RC4'
springVersion = '5.0.0.RELEASE'
javadocLinks = [
'http://docs.oracle.com/javase/8/docs/api/',
"http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/",
@@ -74,11 +73,12 @@ subprojects {
dependency 'org.hamcrest:hamcrest-core:1.3'
dependency 'org.hamcrest:hamcrest-library:1.3'
dependency 'org.hibernate:hibernate-validator:5.4.1.Final'
dependency 'org.jacoco:org.jacoco.agent:0.7.9'
dependency 'org.jruby:jruby-complete:9.1.13.0'
dependency 'org.junit.jupiter:junit-jupiter-api:5.0.0-M4'
dependency 'org.mockito:mockito-core:1.10.19'
dependency 'org.springframework.hateoas:spring-hateoas:0.23.0.RELEASE'
dependency 'org.jacoco:org.jacoco.agent:0.7.9'
dependency 'org.synchronoss.cloud:nio-multipart-parser:1.1.0'
}
}
@@ -168,6 +168,7 @@ samples {
dependOn 'spring-restdocs-core:install'
dependOn 'spring-restdocs-mockmvc:install'
dependOn 'spring-restdocs-restassured:install'
dependOn 'spring-restdocs-webtestclient:install'
dependOn 'spring-restdocs-asciidoctor:install'
restNotesGrails {
@@ -190,6 +191,10 @@ samples {
workingDir "$projectDir/samples/rest-assured"
}
webTestClient {
workingDir "$projectDir/samples/web-test-client"
}
slate {
workingDir "$projectDir/samples/rest-notes-slate"
build false

View File

@@ -73,7 +73,7 @@
<module name="AvoidStarImport" />
<module name="AvoidStaticImport">
<property name="excludes"
value="com.jayway.restassured.RestAssured.*, io.restassured.RestAssured.*, org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.cli.CliDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.restassured.RestAssuredRestDocumentation.*, org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.*, org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.*, org.springframework.restdocs.restassured3.operation.preprocess.RestAssuredPreprocessors.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.templates.TemplateFormats.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" />
value="com.jayway.restassured.RestAssured.*, io.restassured.RestAssured.*, org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.cli.CliDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.restassured.RestAssuredRestDocumentation.*, org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.*, org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.*, org.springframework.restdocs.restassured3.operation.preprocess.RestAssuredPreprocessors.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.templates.TemplateFormats.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*, org.springframework.web.reactive.function.BodyInserters.*, org.springframework.web.reactive.function.server.RequestPredicates.GET, org.springframework.web.reactive.function.server.RequestPredicates.POST" />
</module>
<module name="IllegalImport" />
<module name="RedundantImport" />

View File

@@ -3,15 +3,15 @@ plugins {
}
repositories {
maven {
url 'https://repo.spring.io/release'
}
maven { url 'https://repo.spring.io/release' }
maven { url 'https://repo.spring.io/snapshot' }
}
dependencies {
asciidoctor 'io.spring.asciidoctor:spring-asciidoctor-extensions:0.1.1.RELEASE'
asciidoctor 'io.spring.asciidoctor:spring-asciidoctor-extensions:0.1.2.BUILD-SNAPSHOT'
testCompile project(':spring-restdocs-mockmvc')
testCompile project(':spring-restdocs-restassured')
testCompile project(':spring-restdocs-webtestclient')
testCompile 'io.rest-assured:rest-assured'
testCompile 'javax.validation:validation-api'
testCompile 'junit:junit'

View File

@@ -54,6 +54,12 @@ using the `RestDocumentationConfigurer` API. For example, to use `ISO-8859-1`:
include::{examples-dir}/com/example/mockmvc/CustomEncoding.java[tags=custom-encoding]
----
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/CustomEncoding.java[tags=custom-encoding]
----
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -79,6 +85,12 @@ box. You can change the default format using the `RestDocumentationConfigurer` A
include::{examples-dir}/com/example/mockmvc/CustomFormat.java[tags=custom-format]
----
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/CustomFormat.java[tags=custom-format]
----
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -109,6 +121,12 @@ snippet by default:
include::{examples-dir}/com/example/mockmvc/CustomDefaultSnippets.java[tags=custom-default-snippets]
----
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/CustomDefaultSnippets.java[tags=custom-default-snippets]
----
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -130,6 +148,14 @@ include::{examples-dir}/com/example/mockmvc/CustomDefaultOperationPreprocessors.
<1> Apply a request preprocessor that will remove the header named `Foo`.
<2> Apply a response preprocessor that will pretty print its content.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/CustomDefaultOperationPreprocessors.java[tags=custom-default-operation-preprocessors]
----
<1> Apply a request preprocessor that will remove the header named `Foo`.
<2> Apply a response preprocessor that will pretty print its content.
[source,java,indent=0,role="secondary"]
.REST Assured
----

View File

@@ -18,6 +18,14 @@ include::{examples-dir}/com/example/mockmvc/PerTestPreprocessing.java[tags=prepr
<1> Apply a request preprocessor that will remove the header named `Foo`.
<2> Apply a response preprocessor that will pretty print its content.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/PerTestPreprocessing.java[tags=preprocessing]
----
<1> Apply a request preprocessor that will remove the header named `Foo`.
<2> Apply a response preprocessor that will pretty print its content.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -39,6 +47,14 @@ include::{examples-dir}/com/example/mockmvc/EveryTestPreprocessing.java[tags=set
<1> Apply a request preprocessor that will remove the header named `Foo`.
<2> Apply a response preprocessor that will pretty print its content.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/EveryTestPreprocessing.java[tags=setup]
----
<1> Apply a request preprocessor that will remove the header named `Foo`.
<2> Apply a response preprocessor that will pretty print its content.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -55,6 +71,12 @@ Then, in each test, any configuration specific to that test can be performed. Fo
include::{examples-dir}/com/example/mockmvc/EveryTestPreprocessing.java[tags=use]
----
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/EveryTestPreprocessing.java[tags=use]
----
[source,java,indent=0,role="secondary"]
.REST Assured
----

View File

@@ -23,6 +23,18 @@ include::{examples-dir}/com/example/mockmvc/Hypermedia.java[tag=links]
`org.springframework.restdocs.hypermedia.HypermediaDocumentation`.
<3> Expect a link whose rel is `bravo`.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/Hypermedia.java[tag=links]
----
<1> Configure Spring REST docs to produce a snippet describing the response's links.
Uses the static `links` method on
`org.springframework.restdocs.hypermedia.HypermediaDocumentation`.
<2> Expect a link whose rel is `alpha`. Uses the static `linkWithRel` method on
`org.springframework.restdocs.hypermedia.HypermediaDocumentation`.
<3> Expect a link whose rel is `bravo`.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -77,6 +89,14 @@ include::{examples-dir}/com/example/mockmvc/Hypermedia.java[tag=explicit-extract
<1> Indicate that the links are in HAL format. Uses the static `halLinks` method on
`org.springframework.restdocs.hypermedia.HypermediaDocumentation`.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/Hypermedia.java[tag=explicit-extractor]
----
<1> Indicate that the links are in HAL format. Uses the static `halLinks` method on
`org.springframework.restdocs.hypermedia.HypermediaDocumentation`.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -152,6 +172,18 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=response]
on `org.springframework.restdocs.payload.PayloadDocumentation`.
<3> Expect a field with the path `contact.name`.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/Payload.java[tags=response]
----
<1> Configure Spring REST docs to produce a snippet describing the fields in the response
payload. To document a request `requestFields` can be used. Both are static methods on
`org.springframework.restdocs.payload.PayloadDocumentation`.
<2> Expect a field with the path `contact.email`. Uses the static `fieldWithPath` method
on `org.springframework.restdocs.payload.PayloadDocumentation`.
<3> Expect a field with the path `contact.name`.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -181,7 +213,16 @@ subsection of a payload can be documented. For example:
include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection]
----
<1> Document the subsection with the path `contact`. `contact.email` and `contact.name`
are now seen has having also been documented. Uses the static `subsectionWithPath`
are now seen as having also been documented. Uses the static `subsectionWithPath`
method on `org.springframework.restdocs.payload.PayloadDocumentation`.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/Payload.java[tags=subsection]
----
<1> Document the subsection with the path `contact`. `contact.email` and `contact.name`
are now seen as having also been documented. Uses the static `subsectionWithPath`
method on `org.springframework.restdocs.payload.PayloadDocumentation`.
[source,java,indent=0,role="secondary"]
@@ -190,7 +231,7 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection]
include::{examples-dir}/com/example/restassured/Payload.java[tags=subsection]
----
<1> Document the subsection with the path `contact`. `contact.email` and `contact.name`
are now seen has having also been documented. Uses the static `subsectionWithPath`
are now seen as having also been documented. Uses the static `subsectionWithPath`
method on `org.springframework.restdocs.payload.PayloadDocumentation`.
`subsectionWithPath` can be useful for providing a high-level overview of a particular
@@ -360,14 +401,21 @@ used:
----
include::{examples-dir}/com/example/mockmvc/Payload.java[tags=explicit-type]
----
<1> Set the field's type to `string`.
<1> Set the field's type to `String`.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/Payload.java[tags=explicit-type]
----
<1> Set the field's type to `String`.
[source,java,indent=0,role="secondary"]
.REST Assured
----
include::{examples-dir}/com/example/restassured/Payload.java[tags=explicit-type]
----
<1> Set the field's type to `string`.
<1> Set the field's type to `String`.
[[documenting-your-api-request-response-payloads-fields-xml]]
@@ -443,6 +491,13 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=single-book]
----
<1> Document `title` and `author` using existing descriptors
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/Payload.java[tags=single-book]
----
<1> Document `title` and `author` using existing descriptors
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -460,6 +515,14 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=book-array]
<1> Document the array
<2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].`
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/Payload.java[tags=book-array]
----
<1> Document the array
<2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].`
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -508,6 +571,16 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=body-subsection]
`org.springframework.restdocs.payload.PayloadDocumentation`. To produce a snippet
for the request body, `requestBody` can be used in place of `responseBody`.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/Payload.java[tags=body-subsection]
----
<1> Produce a snippet containing a subsection of the response body. Uses the static
`responseBody` and `beneathPath` methods on
`org.springframework.restdocs.payload.PayloadDocumentation`. To produce a snippet
for the request body, `requestBody` can be used in place of `responseBody`.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -548,7 +621,8 @@ This example will result in a snippet named `request-body-temp.adoc`.
===== Documenting the fields of a subsection of a request or response
As well as documenting a subsection of a request or response body, it's also possible to
document the fields in a particular subsection. A snippet that documents the fields of the `temperature` object (`high` and `low`) can be produced as follows:
document the fields in a particular subsection. A snippet that documents the fields of
the `temperature` object (`high` and `low`) can be produced as follows:
[source,java,indent=0,role="primary"]
.MockMvc
@@ -560,6 +634,16 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=fields-subsection]
`org.springframework.restdocs.payload.PayloadDocumentation`.
<2> Document the `high` and `low` fields.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/Payload.java[tags=fields-subsection]
----
<1> Produce a snippet describing the fields in the subsection of the response payload
beneath the path `weather.temperature`. Uses the static `beneathPath` method on
`org.springframework.restdocs.payload.PayloadDocumentation`.
<2> Document the `high` and `low` fields.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -598,6 +682,20 @@ include::{examples-dir}/com/example/mockmvc/RequestParameters.java[tags=request-
`org.springframework.restdocs.request.RequestDocumentation`.
<4> Document the `per_page` parameter.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/RequestParameters.java[tags=request-parameters-query-string]
----
<1> Perform a `GET` request with two parameters, `page` and `per_page` in the query
string.
<2> Configure Spring REST Docs to produce a snippet describing the request's parameters.
Uses the static `requestParameters` method on
`org.springframework.restdocs.request.RequestDocumentation`.
<3> Document the `page` parameter. Uses the static `parameterWithName` method on
`org.springframework.restdocs.request.RequestDocumentation`.
<4> Document the `per_page` parameter.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -621,6 +719,13 @@ include::{examples-dir}/com/example/mockmvc/RequestParameters.java[tags=request-
----
<1> Perform a `POST` request with a single parameter, `username`.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/RequestParameters.java[tags=request-parameters-form-data]
----
<1> Perform a `POST` request with a single parameter, `username`.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -667,6 +772,19 @@ include::{examples-dir}/com/example/mockmvc/PathParameters.java[tags=path-parame
`org.springframework.restdocs.request.RequestDocumentation`.
<4> Document the parameter named `longitude`.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/PathParameters.java[tags=path-parameters]
----
<1> Perform a `GET` request with two path parameters, `latitude` and `longitude`.
<2> Configure Spring REST Docs to produce a snippet describing the request's path
parameters. Uses the static `pathParameters` method on
`org.springframework.restdocs.request.RequestDocumentation`.
<3> Document the parameter named `latitude`. Uses the static `parameterWithName` method on
`org.springframework.restdocs.request.RequestDocumentation`.
<4> Document the parameter named `longitude`.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -683,9 +801,9 @@ include::{examples-dir}/com/example/restassured/PathParameters.java[tags=path-pa
The result is a snippet named `path-parameters.adoc` that contains a table describing
the path parameters that are supported by the resource.
TIP: To make the path parameters available for documentation, the request must be
built using one of the methods on `RestDocumentationRequestBuilders` rather than
`MockMvcRequestBuilders`.
TIP: If you are using MockMvc then, to make the path parameters available for documentation,
the request must be built using one of the methods on `RestDocumentationRequestBuilders`
rather than `MockMvcRequestBuilders`.
When documenting path parameters, the test will fail if an undocumented path parameter
is used in the request. Similarly, the test will also fail if a documented path parameter
@@ -720,6 +838,18 @@ include::{examples-dir}/com/example/mockmvc/RequestParts.java[tags=request-parts
<3> Document the part named `file`. Uses the static `partWithName` method on
`org.springframework.restdocs.request.RequestDocumentation`.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/RequestParts.java[tags=request-parts]
----
<1> Perform a `POST` request with a single part named `file`.
<2> Configure Spring REST Docs to produce a snippet describing the request's parts. Uses
the static `requestParts` method on
`org.springframework.restdocs.request.RequestDocumentation`.
<3> Document the part named `file`. Uses the static `partWithName` method on
`org.springframework.restdocs.request.RequestDocumentation`.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -775,6 +905,16 @@ include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=body]
`PayloadDocumentation`.
payload.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/RequestPartPayload.java[tags=body]
----
<1> Configure Spring REST docs to produce a snippet containing the body of the
of the request part named `metadata`. Uses the static `requestPartBody` method on
`PayloadDocumentation`.
payload.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -808,6 +948,18 @@ include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=fields]
<2> Expect a field with the path `version`. Uses the static `fieldWithPath` method on
`org.springframework.restdocs.payload.PayloadDocumentation`.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/RequestPartPayload.java[tags=fields]
----
<1> Configure Spring REST docs to produce a snippet describing the fields in the payload
of the request part named `metadata`. Uses the static `requestPartFields` method on
`PayloadDocumentation`.
payload.
<2> Expect a field with the path `version`. Uses the static `fieldWithPath` method on
`org.springframework.restdocs.payload.PayloadDocumentation`.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -863,6 +1015,20 @@ include::{examples-dir}/com/example/mockmvc/HttpHeaders.java[tags=headers]
<4> Produce a snippet describing the response's headers. Uses the static `responseHeaders`
method on `org.springframework.restdocs.headers.HeaderDocumentation`.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/HttpHeaders.java[tags=headers]
----
<1> Perform a `GET` request with an `Authorization` header that uses basic authentication
<2> Configure Spring REST Docs to produce a snippet describing the request's headers.
Uses the static `requestHeaders` method on
`org.springframework.restdocs.headers.HeaderDocumentation`.
<3> Document the `Authorization` header. Uses the static `headerWithName` method on
`org.springframework.restdocs.headers.HeaderDocumentation`.
<4> Produce a snippet describing the response's headers. Uses the static `responseHeaders`
method on `org.springframework.restdocs.headers.HeaderDocumentation`.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -910,6 +1076,14 @@ include::{examples-dir}/com/example/mockmvc/MockMvcSnippetReuse.java[tags=use]
<1> Reuse the `pagingLinks` `Snippet` calling `and` to add descriptors that are specific
to the resource that is being documented.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/WebTestClientSnippetReuse.java[tags=use]
----
<1> Reuse the `pagingLinks` `Snippet` calling `and` to add descriptors that are specific
to the resource that is being documented.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -1062,8 +1236,10 @@ You can configure which snippets are produced by default. Please refer to the
[[documentating-your-api-parameterized-output-directories]]
=== Using parameterized output directories
The output directory used by `document` can be parameterized. The following parameters
are supported:
When using MockMvc or REST Assured, the output directory used by `document` can be
parameterized. The output directory cannot be parameterized when using WebTestClient.
The following parameters are supported:
[cols="1,3"]
|===
@@ -1168,6 +1344,15 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=constraints]
<2> Set the `constraints` attribute for the `name` field
<3> Set the `constraints` attribute for the `email` field
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/Payload.java[tags=constraints]
----
<1> Configure the `title` attribute for the request fields snippet
<2> Set the `constraints` attribute for the `name` field
<3> Set the `constraints` attribute for the `email` field
[source,java,indent=0,role="secondary"]
.REST Assured
----

View File

@@ -28,6 +28,17 @@ If you want to jump straight in, a number of sample applications are available:
|===
[cols="3,2,10"]
.WebTestClient
|===
| Sample | Build system | Description
| {samples}/web-test-client[WebTestClient]
| Gradle
| Demonstrates the use of Spring REST docs with Spring WebFlux's WebTestClient.
|===
[cols="3,2,10"]
.REST Assured
@@ -129,8 +140,8 @@ the configuration are described below.
</build>
----
<1> Add a dependency on `spring-restdocs-mockmvc` in the `test` scope. If you want to use
REST Assured rather than MockMvc, add a dependency on `spring-restdocs-restassured`
instead.
`WebTestClient` or REST Assured rather than MockMvc, add a dependency on
`spring-restdocs-webtestclient` or `spring-restdocs-restassured` respectively instead.
<2> Add the Asciidoctor plugin.
<3> Using `prepare-package` allows the documentation to be
<<getting-started-build-configuration-maven-packaging, included in the package>>.
@@ -248,8 +259,9 @@ from where it will be included in the jar file.
[[getting-started-documentation-snippets]]
=== Generating documentation snippets
Spring REST Docs uses
{spring-framework-docs}/#spring-mvc-test-framework[Spring's MVC Test framework] or
Spring REST Docs uses Spring MVC's
{spring-framework-docs}/testing.html#spring-mvc-test-framework[test framework],
Spring WebFlux's {spring-framework-docs}/testing.html#webtestclient[`WebTestClient`] or
http://www.rest-assured.io[REST Assured] to make requests to the service that you are
documenting. It then produces documentation snippets for the request and the resulting
response.
@@ -301,7 +313,7 @@ The default can be overridden by providing an output directory when creating the
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("custom");
----
Next, provide an `@Before` method to configure MockMvc or REST Assured:
Next, provide an `@Before` method to configure MockMvc, WebTestClient or REST Assured:
[source,java,indent=0,role="primary"]
.MockMvc
@@ -312,6 +324,16 @@ include::{examples-dir}/com/example/mockmvc/ExampleApplicationTests.java[tags=se
instance of this class can be obtained from the static `documentationConfiguration()`
method on `org.springframework.restdocs.mockmvc.MockMvcRestDocumentation`.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/ExampleApplicationTests.java[tags=setup]
----
<1> The `WebTestClient` instance is configured by adding a
`WebTestclientRestDocumentationConfigurer` as an `ExchangeFilterFunction`. An instance of
this class can be obtained from the static `documentationConfiguration()` method on
`org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation`.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -364,7 +386,7 @@ based on your project's build tool:
Next, provide an `@BeforeEach` method to configure MockMvc or REST Assured:
Next, provide a `@BeforeEach` method to configure MockMvc, WebTestClient, or REST Assured:
[source,java,indent=0,role="primary"]
.MockMvc
@@ -375,6 +397,16 @@ include::{examples-dir}/com/example/mockmvc/ExampleApplicationJUnit5Tests.java[t
instance of this class can be obtained from the static `documentationConfiguration()`
method on `org.springframework.restdocs.mockmvc.MockMvcRestDocumentation`.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/ExampleApplicationJUnit5Tests.java[tags=setup]
----
<1> The `WebTestClient` instance is configured by adding a
`WebTestClientRestDocumentationConfigurer` as an `ExchangeFilterFunction`. An instance of
this class can be obtained from the static `documentationConfiguration()` method on
`org.springframework.restdocs.webtestlcient.WebTestClientRestDocumentation`.
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -407,7 +439,7 @@ private ManualRestDocumentation restDocumentation = new ManualRestDocumentation(
Secondly, `ManualRestDocumentation.beforeTest(Class, String)`
must be called before each test. This can be done as part of the method that is
configuring MockMVC or REST Assured:
configuring MockMvc, WebTestClient, or REST Assured:
[source,java,indent=0,role="primary"]
.MockMvc
@@ -415,6 +447,12 @@ configuring MockMVC or REST Assured:
include::{examples-dir}/com/example/mockmvc/ExampleApplicationTestNgTests.java[tags=setup]
----
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/ExampleApplicationTestNgTests.java[tags=setup]
----
[source,java,indent=0,role="secondary"]
.REST Assured
----
@@ -449,6 +487,19 @@ a `RestDocumentationResultHandler`. An instance of this class can be obtained fr
static `document` method on
`org.springframework.restdocs.mockmvc.MockMvcRestDocumentation`.
[source,java,indent=0,role="secondary"]
.WebTestClient
----
include::{examples-dir}/com/example/webtestclient/InvokeService.java[tags=invoke-service]
----
<1> Invoke the root (`/`) of the service and indicate that an `application/json` response
is required.
<2> Assert that the service produced the expected response.
<3> Document the call to the service, writing the snippets into a directory named `index`
that will be located beneath the configured output directory. The snippets are written by
a `Consumer` of the `ExchangeResult`. Such a consumer can be obtained from the static
`document` method on `org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation`.
[source,java,indent=0,role="secondary"]
.REST Assured
----

View File

@@ -13,7 +13,7 @@ Andy Wilkinson
:samples: {source}/samples
:templates: {source}spring-restdocs/src/main/resources/org/springframework/restdocs/templates
:spring-boot-docs: http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle
:spring-framework-docs: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle
:spring-framework-docs: https://docs.spring.io/spring-framework/docs/5.0.x/spring-framework-reference
[[abstract]]

View File

@@ -10,8 +10,9 @@ http://asciidoctor.org[Asciidoctor] by default. Asciidoctor processes plain text
produces HTML, styled and layed out to suit your needs. If you prefer, Spring REST Docs
can also be configured to use Markdown.
Spring REST Docs makes use of snippets produced by tests written with
{spring-framework-docs}/#spring-mvc-test-framework[Spring MVC Test] or
Spring REST Docs makes use of snippets produced by tests written with Spring MVC's
{spring-framework-docs}/testing.html#spring-mvc-test-framework[test framework], Spring
WebFlux's {spring-framework-docs}/testing.html#webtestclient[`WebTestClient`] or
http://www.rest-assured.io[REST Assured 3]. This test-driven approach helps to guarantee
the accuracy of your service's documentation. If a snippet is incorrect the test that
produces it will fail.

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.junit.Before;
import org.junit.Rule;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration;
public class CustomDefaultOperationPreprocessors {
// @formatter:off
@Rule
public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
private WebApplicationContext context;
@SuppressWarnings("unused")
private WebTestClient webTestClient;
@Before
public void setup() {
// tag::custom-default-operation-preprocessors[]
this.webTestClient = WebTestClient.bindToApplicationContext(this.context)
.configureClient()
.filter(documentationConfiguration(this.restDocumentation)
.operationPreprocessors()
.withRequestDefaults(removeHeaders("Foo")) // <1>
.withResponseDefaults(prettyPrint())) // <2>
.build();
// end::custom-default-operation-preprocessors[]
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.junit.Before;
import org.junit.Rule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.restdocs.cli.CliDocumentation.curlRequest;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration;
public class CustomDefaultSnippets {
// @formatter:off
@Rule
public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
@Autowired
private WebApplicationContext context;
@SuppressWarnings("unused")
private WebTestClient webTestClient;
@Before
public void setUp() {
// tag::custom-default-snippets[]
this.webTestClient = WebTestClient.bindToApplicationContext(this.context)
.configureClient().filter(
documentationConfiguration(this.restDocumentation)
.snippets().withDefaults(curlRequest()))
.build();
// end::custom-default-snippets[]
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.junit.Before;
import org.junit.Rule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration;
public class CustomEncoding {
// @formatter:off
@Rule
public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
@Autowired
private WebApplicationContext context;
@SuppressWarnings("unused")
private WebTestClient webTestClient;
@Before
public void setUp() {
// tag::custom-encoding[]
this.webTestClient = WebTestClient.bindToApplicationContext(this.context).configureClient()
.filter(documentationConfiguration(this.restDocumentation)
.snippets().withEncoding("ISO-8859-1"))
.build();
// end::custom-encoding[]
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.junit.Before;
import org.junit.Rule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.restdocs.templates.TemplateFormats;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration;
public class CustomFormat {
// @formatter:off
@Rule
public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
@Autowired
private WebApplicationContext context;
@SuppressWarnings("unused")
private WebTestClient webTestClient;
@Before
public void setUp() {
// tag::custom-format[]
this.webTestClient = WebTestClient.bindToApplicationContext(this.context).configureClient()
.filter(documentationConfiguration(this.restDocumentation)
.snippets().withTemplateFormat(TemplateFormats.markdown()))
.build();
// end::custom-format[]
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.junit.Before;
import org.junit.Rule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
public class CustomUriConfiguration {
@Rule
public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
@Autowired
private WebApplicationContext context;
@SuppressWarnings("unused")
private MockMvc mockMvc;
@Before
public void setUp() {
// tag::custom-uri-configuration[]
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(documentationConfiguration(this.restDocumentation).uris()
.withScheme("https")
.withHost("example.com")
.withPort(443))
.build();
// end::custom-uri-configuration[]
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.junit.Before;
import org.junit.Rule;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration;
public class EveryTestPreprocessing {
// @formatter:off
@Rule
public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
private WebApplicationContext context;
// tag::setup[]
private WebTestClient webTestClient;
@Before
public void setup() {
this.webTestClient = WebTestClient.bindToApplicationContext(this.context)
.configureClient()
.filter(documentationConfiguration(this.restDocumentation)
.operationPreprocessors()
.withRequestDefaults(removeHeaders("Foo")) // <1>
.withResponseDefaults(prettyPrint())) // <2>
.build();
}
// end::setup[]
public void use() throws Exception {
// tag::use[]
this.webTestClient.get().uri("/").exchange().expectStatus().isOk()
.expectBody().consumeWith(document("index",
links(linkWithRel("self").description("Canonical self link"))));
// end::use[]
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration;
@ExtendWith(RestDocumentationExtension.class)
public class ExampleApplicationJUnit5Tests {
@SuppressWarnings("unused")
// tag::setup[]
private WebTestClient webTestClient;
@BeforeEach
public void setUp(WebApplicationContext webApplicationContext,
RestDocumentationContextProvider restDocumentation) {
this.webTestClient = WebTestClient.bindToApplicationContext(webApplicationContext)
.configureClient()
.filter(documentationConfiguration(restDocumentation)) // <1>
.build();
}
// end::setup[]
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import java.lang.reflect.Method;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.restdocs.ManualRestDocumentation;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration;
public class ExampleApplicationTestNgTests {
public final ManualRestDocumentation restDocumentation = new ManualRestDocumentation();
@SuppressWarnings("unused")
// tag::setup[]
private WebTestClient webTestClient;
@Autowired
private WebApplicationContext context;
@BeforeMethod
public void setUp(Method method) {
this.webTestClient = WebTestClient.bindToApplicationContext(this.context)
.configureClient()
.filter(documentationConfiguration(this.restDocumentation)) // <1>
.build();
this.restDocumentation.beforeTest(getClass(), method.getName());
}
// end::setup[]
// tag::teardown[]
@AfterMethod
public void tearDown() {
this.restDocumentation.afterTest();
}
// end::teardown[]
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.junit.Before;
import org.junit.Rule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration;
public class ExampleApplicationTests {
@Rule
public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
@SuppressWarnings("unused")
// tag::setup[]
private WebTestClient webTestClient;
@Autowired
private WebApplicationContext context;
@Before
public void setUp() {
this.webTestClient = WebTestClient.bindToApplicationContext(this.context)
.configureClient()
.filter(documentationConfiguration(this.restDocumentation)) // <1>
.build();
}
// end::setup[]
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders;
import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
public class HttpHeaders {
// @formatter:off
private WebTestClient webTestClient;
public void headers() throws Exception {
// tag::headers[]
this.webTestClient
.get().uri("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=") // <1>
.exchange().expectStatus().isOk().expectBody()
.consumeWith(document("headers",
requestHeaders( // <2>
headerWithName("Authorization").description("Basic auth credentials")), // <3>
responseHeaders( // <4>
headerWithName("X-RateLimit-Limit")
.description("The total number of requests permitted per period"),
headerWithName("X-RateLimit-Remaining")
.description("Remaining requests permitted in current period"),
headerWithName("X-RateLimit-Reset")
.description("Time at which the rate limit period will reset"))));
// end::headers[]
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.halLinks;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
public class Hypermedia {
private WebTestClient webTestClient;
public void defaultExtractor() throws Exception {
// tag::links[]
this.webTestClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange()
.expectStatus().isOk().expectBody()
.consumeWith(document("index",links( // <1>
linkWithRel("alpha").description("Link to the alpha resource"), // <2>
linkWithRel("bravo").description("Link to the bravo resource")))); // <3>
// end::links[]
}
public void explicitExtractor() throws Exception {
this.webTestClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange()
.expectStatus().isOk().expectBody()
// tag::explicit-extractor[]
.consumeWith(document("index",links(halLinks(), // <1>
linkWithRel("alpha").description("Link to the alpha resource"),
linkWithRel("bravo").description("Link to the bravo resource"))));
// end::explicit-extractor[]
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
public class InvokeService {
private WebTestClient webTestClient;
public void invokeService() throws Exception {
// tag::invoke-service[]
this.webTestClient.get().uri("/").accept(MediaType.APPLICATION_JSON) // <1>
.exchange().expectStatus().isOk() // <2>
.expectBody().consumeWith(document("index")); // <3>
// end::invoke-service[]
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
public class PathParameters {
// @formatter:off
private WebTestClient webTestClient;
public void pathParametersSnippet() throws Exception {
// tag::path-parameters[]
this.webTestClient.get().uri("/locations/{latitude}/{longitude}", 51.5072, 0.1275) // <1>
.exchange().expectStatus().isOk().expectBody()
.consumeWith(document("locations",
pathParameters( // <2>
parameterWithName("latitude").description("The location's latitude"), // <3>
parameterWithName("longitude").description("The location's longitude")))); // <4>
// end::path-parameters[]
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.springframework.http.MediaType;
import org.springframework.restdocs.payload.FieldDescriptor;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
public class Payload {
// @formatter:off
private WebTestClient webTestClient;
public void response() throws Exception {
// tag::response[]
this.webTestClient.get().uri("user/5").accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk().expectBody()
.consumeWith(document("user",
responseFields( // <1>
fieldWithPath("contact.email").description("The user's email address"), // <2>
fieldWithPath("contact.name").description("The user's name")))); // <3>
// end::response[]
}
public void subsection() throws Exception {
// tag::subsection[]
this.webTestClient.get().uri("user/5").accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk().expectBody()
.consumeWith(document("user",
responseFields(
subsectionWithPath("contact").description("The user's contact details")))); // <1>
// end::subsection[]
}
public void explicitType() throws Exception {
this.webTestClient.get().uri("user/5").accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk().expectBody()
// tag::explicit-type[]
.consumeWith(document("user",
responseFields(
fieldWithPath("contact.email")
.type(JsonFieldType.STRING) // <1>
.description("The user's email address"))));
// end::explicit-type[]
}
public void constraints() throws Exception {
this.webTestClient.get().uri("user/5").accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk().expectBody()
// tag::constraints[]
.consumeWith(document("create-user",
requestFields(
attributes(key("title").value("Fields for user creation")), // <1>
fieldWithPath("name")
.description("The user's name")
.attributes(key("constraints").value("Must not be null. Must not be empty")), // <2>
fieldWithPath("email")
.description("The user's email address")
.attributes(key("constraints").value("Must be a valid email address"))))); // <3>
// end::constraints[]
}
public void descriptorReuse() throws Exception {
FieldDescriptor[] book = new FieldDescriptor[] {
fieldWithPath("title").description("Title of the book"),
fieldWithPath("author").description("Author of the book") };
// tag::single-book[]
this.webTestClient.get().uri("/books/1").accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk().expectBody()
.consumeWith(document("book",
responseFields(book))); // <1>
// end::single-book[]
// tag::book-array[]
this.webTestClient.get().uri("/books").accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk().expectBody()
.consumeWith(document("books",
responseFields(
fieldWithPath("[]")
.description("An array of books")) // <1>
.andWithPrefix("[].", book))); // <2>
// end::book-array[]
}
public void fieldsSubsection() throws Exception {
// tag::fields-subsection[]
this.webTestClient.get().uri("/locations/1").accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk().expectBody()
.consumeWith(document("temperature",
responseFields(beneathPath("weather.temperature"), // <1>
fieldWithPath("high").description("The forecast high in degrees celcius"), // <2>
fieldWithPath("low").description("The forecast low in degrees celcius"))));
// end::fields-subsection[]
}
public void bodySubsection() throws Exception {
// tag::body-subsection[]
this.webTestClient.get().uri("/locations/1").accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk().expectBody()
.consumeWith(document("temperature",
responseBody(beneathPath("weather.temperature")))); // <1>
// end::body-subsection[]
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
public class PerTestPreprocessing {
// @formatter:off
private WebTestClient webTestClient;
public void general() {
// tag::preprocessing[]
this.webTestClient.get().uri("/").exchange().expectStatus().isOk().expectBody()
.consumeWith(document("index",
preprocessRequest(removeHeaders("Foo")), // <1>
preprocessResponse(prettyPrint()))); // <2>
// end::preprocessing[]
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
public class RequestParameters {
// @formatter:off
private WebTestClient webTestClient;
public void getQueryStringSnippet() throws Exception {
// tag::request-parameters-query-string[]
this.webTestClient.get().uri("/users?page=2&per_page=100") // <1>
.exchange().expectStatus().isOk().expectBody()
.consumeWith(document("users", requestParameters( // <2>
parameterWithName("page").description("The page to retrieve"), // <3>
parameterWithName("per_page").description("Entries per page") // <4>
)));
// end::request-parameters-query-string[]
}
public void postFormDataSnippet() throws Exception {
// tag::request-parameters-form-data[]
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("username", "Tester");
this.webTestClient.post().uri("/users").body(BodyInserters.fromFormData(formData)) // <1>
.exchange().expectStatus().isCreated().expectBody()
.consumeWith(document("create-user", requestParameters(
parameterWithName("username").description("The user's username")
)));
// end::request-parameters-form-data[]
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import java.util.Collections;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
public class RequestPartPayload {
// @formatter:off
private WebTestClient webTestClient;
public void fields() throws Exception {
// tag::fields[]
MultiValueMap<String, Object> multipartData = new LinkedMultiValueMap<>();
Resource imageResource = new ByteArrayResource("<<png data>>".getBytes()) {
@Override
public String getFilename() {
return "image.png";
}
};
multipartData.add("image", imageResource);
multipartData.add("metadata", Collections.singletonMap("version", "1.0"));
this.webTestClient.post().uri("/images").syncBody(multipartData)
.accept(MediaType.APPLICATION_JSON).exchange()
.expectStatus().isOk().expectBody()
.consumeWith(document("image-upload",
requestPartFields("metadata", // <1>
fieldWithPath("version").description("The version of the image")))); // <2>
// end::fields[]
}
public void body() throws Exception {
// tag::body[]
MultiValueMap<String, Object> multipartData = new LinkedMultiValueMap<>();
Resource imageResource = new ByteArrayResource("<<png data>>".getBytes()) {
@Override
public String getFilename() {
return "image.png";
}
};
multipartData.add("image", imageResource);
multipartData.add("metadata", Collections.singletonMap("version", "1.0"));
this.webTestClient.post().uri("/images").syncBody(multipartData)
.accept(MediaType.APPLICATION_JSON).exchange()
.expectStatus().isOk().expectBody()
.consumeWith(document("image-upload",
requestPartBody("metadata"))); // <1>
// end::body[]
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static org.springframework.restdocs.request.RequestDocumentation.partWithName;
import static org.springframework.restdocs.request.RequestDocumentation.requestParts;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
public class RequestParts {
// @formatter:off
private WebTestClient webTestClient;
public void upload() throws Exception {
// tag::request-parts[]
MultiValueMap<String, Object> multipartData = new LinkedMultiValueMap<>();
multipartData.add("file", "example".getBytes());
this.webTestClient.post().uri("/upload").syncBody(multipartData) // <1>
.exchange().expectStatus().isOk().expectBody()
.consumeWith(document("upload", requestParts( // <2>
partWithName("file").description("The file to upload")) // <3>
));
// end::request-parts[]
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import com.example.SnippetReuse;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
public class WebTestClientSnippetReuse extends SnippetReuse {
// @formatter:off
private WebTestClient webTestClient;
public void documentation() throws Exception {
// tag::use[]
this.webTestClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange()
.expectStatus().isOk().expectBody()
.consumeWith(document("example", this.pagingLinks.and( // <1>
linkWithRel("alpha").description("Link to the alpha resource"),
linkWithRel("bravo").description("Link to the bravo resource"))));
// end::use[]
}
}

View File

@@ -0,0 +1,52 @@
plugins {
id "org.asciidoctor.convert" version "1.5.3"
}
apply plugin: 'java'
apply plugin: 'eclipse'
repositories {
mavenLocal()
maven { url 'https://repo.spring.io/libs-snapshot' }
mavenCentral()
}
group = 'com.example'
sourceCompatibility = 1.8
targetCompatibility = 1.8
ext {
snippetsDir = file('build/generated-snippets')
}
ext['spring-restdocs.version'] = '2.0.0.BUILD-SNAPSHOT'
dependencies {
compile 'io.projectreactor.ipc:reactor-netty:0.7.1.RELEASE'
compile 'org.springframework:spring-context:5.0.1.RELEASE'
compile 'org.springframework:spring-webflux:5.0.1.RELEASE'
testCompile 'junit:junit:4.12'
testCompile 'org.springframework:spring-test:5.0.1.RELEASE'
testCompile "org.springframework.restdocs:spring-restdocs-webtestclient:${project.ext['spring-restdocs.version']}"
asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:${project.ext['spring-restdocs.version']}"
}
test {
outputs.dir snippetsDir
}
asciidoctor {
inputs.dir snippetsDir
dependsOn test
}
jar {
dependsOn asciidoctor
from ("${asciidoctor.outputDir}/html5") {
into 'static/docs'
}
}
eclipseJdt.onlyIf { false }
cleanEclipseJdt.onlyIf { false }

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Thu Mar 23 11:48:10 GMT 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-bin.zip

172
samples/web-test-client/gradlew vendored Executable file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save ( ) {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
samples/web-test-client/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,33 @@
= Spring REST Docs WebTestClient Sample
Andy Wilkinson;
:doctype: book
:icons: font
:source-highlighter: highlightjs
Sample application demonstrating how to use Spring REST Docs with Spring Framework's
WebTestClient.
`SampleWebTestClientApplicationTests` makes a call to a very simple service. Six
snippets are produced. One showing how to make a request using cURL:
include::{snippets}/sample/curl-request.adoc[]
One showing how to make a request using HTTPie:
include::{snippets}/sample/httpie-request.adoc[]
One showing the HTTP request:
include::{snippets}/sample/http-request.adoc[]
One showing the request body:
include::{snippets}/sample/request-body.adoc[]
One showing the HTTP response:
include::{snippets}/sample/http-response.adoc[]
And one showing the response body:
include::{snippets}/sample/response-body.adoc[]

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.ipc.netty.http.server.HttpServer;
@EnableWebFlux
@Configuration
public class SampleWebTestClientApplication {
@Bean
public RouterFunction<ServerResponse> routerFunction() {
return RouterFunctions.route(RequestPredicates.GET("/"), (request) -> ServerResponse.status(HttpStatus.OK).syncBody("Hello, World"));
}
public static void main(String[] args) {
RouterFunction<?> routerFunction = new AnnotationConfigApplicationContext(SampleWebTestClientApplication.class).getBean(RouterFunction.class);
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(RouterFunctions.toHttpHandler(routerFunction));
HttpServer httpServer = HttpServer.create(8080);
httpServer.startAndAwait(adapter);
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.webtestclient;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration;
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = SampleWebTestClientApplication.class)
public class SampleWebTestClientApplicationTests {
@Rule
public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
@Autowired
ApplicationContext context;
private WebTestClient webTestClient;
@Before
public void setUp() {
this.webTestClient = WebTestClient.bindToApplicationContext(context)
.configureClient().baseUrl("https://api.example.com")
.filter(documentationConfiguration(restDocumentation))
.build();
}
@Test
public void sample() throws Exception {
this.webTestClient.get().uri("/").exchange()
.expectStatus().isOk().expectBody()
.consumeWith(document("sample"));
}
}

View File

@@ -5,3 +5,4 @@ include 'spring-restdocs-asciidoctor'
include 'spring-restdocs-core'
include 'spring-restdocs-mockmvc'
include 'spring-restdocs-restassured'
include 'spring-restdocs-webtestclient'

View File

@@ -23,7 +23,6 @@ import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.TestExtensionContext;
@@ -59,14 +58,14 @@ public class RestDocumentationExtension implements Extension, BeforeEachCallback
@Override
public boolean supports(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
ExtensionContext extensionContext) {
return RestDocumentationContextProvider.class
.isAssignableFrom(parameterContext.getParameter().getType());
}
@Override
public Object resolve(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
ExtensionContext extensionContext) {
return this;
}

View File

@@ -22,6 +22,7 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.OperationRequest;
@@ -242,12 +243,13 @@ public final class RestDocumentationGenerator<REQ, RESP> {
private OperationRequest preprocessRequest(OperationRequest request,
Map<String, Object> configuration) {
List<OperationRequestPreprocessor> requestPreprocessors = getRequestPreprocessors(
configuration);
for (OperationRequestPreprocessor preprocessor : requestPreprocessors) {
request = preprocessor.preprocess(request);
}
return request;
return preprocess(getRequestPreprocessors(configuration), request,
this::preprocess);
}
private OperationRequest preprocess(OperationRequestPreprocessor preprocessor,
OperationRequest request) {
return preprocessor.preprocess(request);
}
private List<OperationRequestPreprocessor> getRequestPreprocessors(
@@ -259,11 +261,13 @@ public final class RestDocumentationGenerator<REQ, RESP> {
private OperationResponse preprocessResponse(OperationResponse response,
Map<String, Object> configuration) {
for (OperationResponsePreprocessor preprocessor : getResponsePreprocessors(
configuration)) {
response = preprocessor.preprocess(response);
}
return response;
return preprocess(getResponsePreprocessors(configuration), response,
this::preprocess);
}
private OperationResponse preprocess(OperationResponsePreprocessor preprocessor,
OperationResponse response) {
return preprocessor.preprocess(response);
}
private List<OperationResponsePreprocessor> getResponsePreprocessors(
@@ -273,6 +277,15 @@ public final class RestDocumentationGenerator<REQ, RESP> {
configuration);
}
private <P, T> T preprocess(List<P> preprocessors, T target,
BiFunction<P, T, T> function) {
T processed = target;
for (P preprocessor : preprocessors) {
processed = function.apply(preprocessor, processed);
}
return processed;
}
@SuppressWarnings("unchecked")
private <T> List<T> getPreprocessors(T preprocessor, String preprocessorAttribute,
Map<String, Object> configuration) {

View File

@@ -49,11 +49,6 @@ final class JsonFieldProcessor {
matches.add(match.getValue());
}
@Override
public void absent() {
}
});
if (matches.isEmpty()) {
throw new FieldDoesNotExistException(path);
@@ -72,11 +67,6 @@ final class JsonFieldProcessor {
match.remove();
}
@Override
public void absent() {
}
});
}
@@ -89,11 +79,6 @@ final class JsonFieldProcessor {
match.removeSubsection();
}
@Override
public void absent() {
}
});
}
@@ -376,7 +361,8 @@ final class JsonFieldProcessor {
void foundMatch(Match match);
void absent();
default void absent() {
}
}

View File

@@ -0,0 +1,17 @@
description = 'Spring REST Docs WebFlux'
dependencies {
compile 'org.springframework:spring-test'
compile 'org.springframework:spring-webflux'
compile project(':spring-restdocs-core')
testCompile 'org.mockito:mockito-core'
testCompile 'org.hamcrest:hamcrest-library'
testCompile project(path: ':spring-restdocs-core', configuration: 'testArtifacts')
testRuntime 'org.springframework:spring-context'
testRuntime 'org.synchronoss.cloud:nio-multipart-parser'
}
test {
jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*"
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.webtestclient;
import reactor.core.publisher.Mono;
import org.springframework.restdocs.config.OperationPreprocessorsConfigurer;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
/**
* A configurer that can be used to configure the operation preprocessors.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public final class WebTestClientOperationPreprocessorsConfigurer extends
OperationPreprocessorsConfigurer<WebTestClientRestDocumentationConfigurer, WebTestClientOperationPreprocessorsConfigurer>
implements ExchangeFilterFunction {
WebTestClientOperationPreprocessorsConfigurer(WebTestClientRestDocumentationConfigurer parent) {
super(parent);
}
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
return and().filter(request, next);
}
}

View File

@@ -0,0 +1,154 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.webtestclient;
import java.io.ByteArrayOutputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import reactor.core.publisher.Flux;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.codec.FormHttpMessageReader;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
import org.springframework.http.codec.multipart.Part;
import org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestFactory;
import org.springframework.restdocs.operation.OperationRequestPart;
import org.springframework.restdocs.operation.OperationRequestPartFactory;
import org.springframework.restdocs.operation.Parameters;
import org.springframework.restdocs.operation.QueryStringParser;
import org.springframework.restdocs.operation.RequestConverter;
import org.springframework.restdocs.operation.RequestCookie;
import org.springframework.test.web.reactive.server.ExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.ClassUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* A {@link RequestConverter} for creating an {@link OperationRequest} derived from an
* {@link ExchangeResult}.
*
* @author Andy Wilkinson
*/
class WebTestClientRequestConverter implements RequestConverter<ExchangeResult> {
private static final ResolvableType FORM_DATA_TYPE = ResolvableType
.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
private final QueryStringParser queryStringParser = new QueryStringParser();
private final FormHttpMessageReader formDataReader = new FormHttpMessageReader();
@Override
public OperationRequest convert(ExchangeResult result) {
return new OperationRequestFactory().create(result.getUrl(), result.getMethod(),
result.getRequestBodyContent(), extractRequestHeaders(result),
extractParameters(result), extractRequestParts(result),
extractCookies(result));
}
private HttpHeaders extractRequestHeaders(ExchangeResult result) {
HttpHeaders extracted = new HttpHeaders();
extracted.putAll(result.getRequestHeaders());
extracted.remove(WebTestClient.WEBTESTCLIENT_REQUEST_ID);
return extracted;
}
private Parameters extractParameters(ExchangeResult result) {
if (result.getMethod() == HttpMethod.GET) {
return this.queryStringParser.parse(result.getUrl());
}
Parameters parameters = new Parameters();
if (result.getRequestHeaders().getContentType()
.equals(MediaType.APPLICATION_FORM_URLENCODED)) {
parameters.addAll(this.formDataReader
.readMono(FORM_DATA_TYPE,
new ExchangeResultReactiveHttpInputMessage(result), null)
.block());
}
return parameters;
}
private List<OperationRequestPart> extractRequestParts(ExchangeResult result) {
if (!ClassUtils.isPresent(
"org.synchronoss.cloud.nio.multipart.NioMultipartParserListener",
getClass().getClassLoader())) {
return Collections.emptyList();
}
return new MultipartHttpMessageReader(new SynchronossPartHttpMessageReader())
.readMono(null, new ExchangeResultReactiveHttpInputMessage(result), null)
.onErrorReturn(new LinkedMultiValueMap<>()).block().values().stream()
.flatMap((parts) -> parts.stream().map(this::createOperationRequestPart))
.collect(Collectors.toList());
}
private OperationRequestPart createOperationRequestPart(Part part) {
ByteArrayOutputStream content = readPartBodyContent(part);
return new OperationRequestPartFactory().create(part.name(),
part instanceof FilePart ? ((FilePart) part).filename() : null,
content.toByteArray(), part.headers());
}
private ByteArrayOutputStream readPartBodyContent(Part part) {
ByteArrayOutputStream contentStream = new ByteArrayOutputStream();
DataBufferUtils.write(part.content(), contentStream).blockFirst();
return contentStream;
}
private Collection<RequestCookie> extractCookies(ExchangeResult result) {
// Cookies are not available. See https://jira.spring.io/browse/SPR-16124.
return Collections.emptyList();
}
private final class ExchangeResultReactiveHttpInputMessage
implements ReactiveHttpInputMessage {
private final ExchangeResult result;
private ExchangeResultReactiveHttpInputMessage(ExchangeResult result) {
this.result = result;
}
@Override
public HttpHeaders getHeaders() {
return this.result.getRequestHeaders();
}
@Override
public Flux<DataBuffer> getBody() {
DefaultDataBuffer buffer = new DefaultDataBufferFactory().allocateBuffer();
buffer.write(this.result.getRequestBodyContent());
return Flux.fromArray(new DataBuffer[] { buffer });
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.webtestclient;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.OperationResponseFactory;
import org.springframework.restdocs.operation.ResponseConverter;
import org.springframework.test.web.reactive.server.ExchangeResult;
/**
* A {@link ResponseConverter} for creating an {@link OperationResponse} derived from an
* {@link ExchangeResult}.
*
* @author Andy Wilkinson
*/
class WebTestClientResponseConverter implements ResponseConverter<ExchangeResult> {
@Override
public OperationResponse convert(ExchangeResult result) {
return new OperationResponseFactory().create(result.getStatus(),
result.getResponseHeaders(), result.getResponseBodyContent());
}
}

View File

@@ -0,0 +1,154 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.webtestclient;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.generate.RestDocumentationGenerator;
import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor;
import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.test.web.reactive.server.ExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.reactive.server.WebTestClient.BodyContentSpec;
import org.springframework.test.web.reactive.server.WebTestClient.BodySpec;
import org.springframework.test.web.reactive.server.WebTestClient.Builder;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
/**
* Static factory methods for documenting RESTful APIs using WebFlux's
* {@link WebTestClient}.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public abstract class WebTestClientRestDocumentation {
private static final WebTestClientRequestConverter REQUEST_CONVERTER = new WebTestClientRequestConverter();
private static final WebTestClientResponseConverter RESPONSE_CONVERTER = new WebTestClientResponseConverter();
private WebTestClientRestDocumentation() {
}
/**
* Provides access to a {@link ExchangeFilterFunction} that can be used to configure a
* {@link WebTestClient} instance using the given {@code contextProvider}.
*
* @param contextProvider the context provider
* @return the configurer
* @see Builder#filter(ExchangeFilterFunction)
*/
public static WebTestClientRestDocumentationConfigurer documentationConfiguration(
RestDocumentationContextProvider contextProvider) {
return new WebTestClientRestDocumentationConfigurer(contextProvider);
}
/**
* Returns a {@link Consumer} that, when called, documents the API call with the given
* {@code identifier} using the given {@code snippets} in addition to any default
* snippets.
*
* @param identifier an identifier for the API call that is being documented
* @param snippets the snippets
* @param <T> the type of {@link ExchangeResult} that will be consumed
* @return the {@link Consumer} that will document the API call represented by the
* {@link ExchangeResult}.
* @see BodySpec#consumeWith(Consumer)
* @see BodyContentSpec#consumeWith(Consumer)
*/
public static <T extends ExchangeResult> Consumer<T> document(String identifier,
Snippet... snippets) {
return (result) -> new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER,
RESPONSE_CONVERTER, snippets).handle(result, result,
retrieveConfiguration(result));
}
/**
* Documents the API call with the given {@code identifier} using the given
* {@code snippets} in addition to any default snippets. The given
* {@code requestPreprocessor} is applied to the request before it is documented.
*
* @param identifier an identifier for the API call that is being documented
* @param requestPreprocessor the request preprocessor
* @param snippets the snippets
* @param <T> the type of {@link ExchangeResult} that will be consumed
* @return the {@link Consumer} that will document the API call represented by the
* {@link ExchangeResult}.
*/
public static <T extends ExchangeResult> Consumer<T> document(String identifier,
OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) {
return (result) -> new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER,
RESPONSE_CONVERTER, requestPreprocessor, snippets).handle(result, result,
retrieveConfiguration(result));
}
/**
* Documents the API call with the given {@code identifier} using the given
* {@code snippets} in addition to any default snippets. The given
* {@code responsePreprocessor} is applied to the request before it is documented.
*
* @param identifier an identifier for the API call that is being documented
* @param responsePreprocessor the response preprocessor
* @param snippets the snippets
* @param <T> the type of {@link ExchangeResult} that will be consumed
* @return the {@link Consumer} that will document the API call represented by the
* {@link ExchangeResult}.
*/
public static <T extends ExchangeResult> Consumer<T> document(String identifier,
OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) {
return (result) -> new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER,
RESPONSE_CONVERTER, responsePreprocessor, snippets).handle(result, result,
retrieveConfiguration(result));
}
/**
* Documents the API call with the given {@code identifier} using the given
* {@code snippets} in addition to any default snippets. The given
* {@code requestPreprocessor} and {@code responsePreprocessor} are applied to the
* request and response respectively before they are documented.
*
* @param identifier an identifier for the API call that is being documented
* @param requestPreprocessor the request preprocessor
* @param responsePreprocessor the response preprocessor
* @param snippets the snippets
* @param <T> the type of {@link ExchangeResult} that will be consumed
* @return the {@link Consumer} that will document the API call represented by the
* {@link ExchangeResult}.
*/
public static <T extends ExchangeResult> Consumer<T> document(String identifier,
OperationRequestPreprocessor requestPreprocessor,
OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) {
return (result) -> new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER,
RESPONSE_CONVERTER, requestPreprocessor, responsePreprocessor, snippets)
.handle(result, result, retrieveConfiguration(result));
}
private static Map<String, Object> retrieveConfiguration(ExchangeResult result) {
Map<String, Object> configuration = new HashMap<>(
WebTestClientRestDocumentationConfigurer
.retrieveConfiguration(result.getRequestHeaders()));
configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE,
result.getUriTemplate());
return configuration;
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.webtestclient;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.restdocs.RestDocumentationContext;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.config.RestDocumentationConfigurer;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
/**
* A WebFlux-specific {@link RestDocumentationConfigurer}.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class WebTestClientRestDocumentationConfigurer extends
RestDocumentationConfigurer<WebTestClientSnippetConfigurer, WebTestClientOperationPreprocessorsConfigurer, WebTestClientRestDocumentationConfigurer>
implements ExchangeFilterFunction {
private final WebTestClientSnippetConfigurer snippetConfigurer = new WebTestClientSnippetConfigurer(
this);
private static final Map<String, Map<String, Object>> configurations = new ConcurrentHashMap<>();
private final WebTestClientOperationPreprocessorsConfigurer operationPreprocessorsConfigurer = new WebTestClientOperationPreprocessorsConfigurer(
this);
private final RestDocumentationContextProvider contextProvider;
WebTestClientRestDocumentationConfigurer(RestDocumentationContextProvider contextProvider) {
this.contextProvider = contextProvider;
}
@Override
public WebTestClientSnippetConfigurer snippets() {
return this.snippetConfigurer;
}
@Override
public WebTestClientOperationPreprocessorsConfigurer operationPreprocessors() {
return this.operationPreprocessorsConfigurer;
}
private Map<String, Object> createConfiguration() {
RestDocumentationContext context = this.contextProvider.beforeOperation();
Map<String, Object> configuration = new HashMap<>();
configuration.put(RestDocumentationContext.class.getName(), context);
apply(configuration, context);
return configuration;
}
static Map<String, Object> retrieveConfiguration(HttpHeaders headers) {
String requestId = headers.getFirst(WebTestClient.WEBTESTCLIENT_REQUEST_ID);
return configurations.remove(requestId);
}
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
String index = request.headers().getFirst(WebTestClient.WEBTESTCLIENT_REQUEST_ID);
configurations.put(index, createConfiguration());
return next.exchange(request);
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.webtestclient;
import reactor.core.publisher.Mono;
import org.springframework.restdocs.config.SnippetConfigurer;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
/**
* A {@link SnippetConfigurer} for WebFlux that can be used to configure the generated
* documentation snippets.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class WebTestClientSnippetConfigurer extends
SnippetConfigurer<WebTestClientRestDocumentationConfigurer, WebTestClientSnippetConfigurer>
implements ExchangeFilterFunction {
WebTestClientSnippetConfigurer(WebTestClientRestDocumentationConfigurer parent) {
super(parent);
}
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
return and().filter(request, next);
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Core classes for using Spring REST Docs with Spring Framework's WebTestClient.
*/
package org.springframework.restdocs.webtestclient;

View File

@@ -0,0 +1,208 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.webtestclient;
import java.net.URI;
import java.util.Arrays;
import org.junit.Test;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestPart;
import org.springframework.test.web.reactive.server.ExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyExtractors;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.RouterFunctions;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.Matchers.hasEntry;
import static org.junit.Assert.assertThat;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
/**
* Tests for {@link WebTestClientRequestConverter}.
*
* @author Andy Wilkinson
*/
public class WebTestClientRequestConverterTests {
private final WebTestClientRequestConverter converter = new WebTestClientRequestConverter();
@Test
public void httpRequest() {
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null))
.configureClient().baseUrl("http://localhost").build().get().uri("/foo")
.exchange().expectBody().returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(), is(URI.create("http://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void httpRequestWithCustomPort() {
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null))
.configureClient().baseUrl("http://localhost:8080").build().get()
.uri("/foo").exchange().expectBody().returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(), is(URI.create("http://localhost:8080/foo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void requestWithHeaders() {
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(GET("/"), (req) -> null))
.configureClient().baseUrl("http://localhost").build().get().uri("/foo")
.header("a", "alpha", "apple").header("b", "bravo").exchange()
.expectBody().returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(), is(URI.create("http://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
assertThat(request.getHeaders(), hasEntry("a", Arrays.asList("alpha", "apple")));
assertThat(request.getHeaders(), hasEntry("b", Arrays.asList("bravo")));
}
@Test
public void httpsRequest() {
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null))
.configureClient().baseUrl("https://localhost").build().get().uri("/foo")
.exchange().expectBody().returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(), is(URI.create("https://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void httpsRequestWithCustomPort() {
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null))
.configureClient().baseUrl("https://localhost:8443").build().get()
.uri("/foo").exchange().expectBody().returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(), is(URI.create("https://localhost:8443/foo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void getRequestWithQueryStringPopulatesParameters() throws Exception {
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null))
.configureClient().baseUrl("http://localhost").build().get()
.uri("/foo?a=alpha&b=bravo").exchange().expectBody().returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(),
is(URI.create("http://localhost/foo?a=alpha&b=bravo")));
assertThat(request.getParameters().size(), is(2));
assertThat(request.getParameters(), hasEntry("a", Arrays.asList("alpha")));
assertThat(request.getParameters(), hasEntry("b", Arrays.asList("bravo")));
assertThat(request.getMethod(), is(HttpMethod.GET));
}
@Test
public void postRequestWithParameters() throws Exception {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.addAll("a", Arrays.asList("alpha", "apple"));
parameters.addAll("b", Arrays.asList("br&vo"));
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(POST("/foo"), (req) -> {
req.body(BodyExtractors.toFormData()).block();
return null;
})).configureClient().baseUrl("http://localhost").build().post()
.uri("/foo").body(BodyInserters.fromFormData(parameters)).exchange()
.expectBody().returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(), is(URI.create("http://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.POST));
assertThat(request.getParameters().size(), is(2));
assertThat(request.getParameters(),
hasEntry("a", Arrays.asList("alpha", "apple")));
assertThat(request.getParameters(), hasEntry("b", Arrays.asList("br&vo")));
}
@Test
public void multipartUpload() throws Exception {
MultiValueMap<String, Object> multipartData = new LinkedMultiValueMap<>();
multipartData.add("file", new byte[] { 1, 2, 3, 4 });
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(POST("/foo"), (req) -> {
req.body(BodyExtractors.toMultipartData()).block();
return null;
})).configureClient().baseUrl("http://localhost").build().post()
.uri("/foo").syncBody(multipartData).exchange().expectBody()
.returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(), is(URI.create("http://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.POST));
assertThat(request.getParts().size(), is(1));
OperationRequestPart part = request.getParts().iterator().next();
assertThat(part.getName(), is(equalTo("file")));
assertThat(part.getSubmittedFileName(), is(nullValue()));
assertThat(part.getHeaders().size(), is(2));
assertThat(part.getHeaders().getContentLength(), is(4L));
assertThat(part.getHeaders().getContentDisposition().getName(),
is(equalTo("file")));
assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 })));
}
@Test
public void multipartUploadFromResource() throws Exception {
MultiValueMap<String, Object> multipartData = new LinkedMultiValueMap<>();
multipartData.add("file", new ByteArrayResource(new byte[] { 1, 2, 3, 4 }) {
@Override
public String getFilename() {
return "image.png";
}
});
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(POST("/foo"), (req) -> {
req.body(BodyExtractors.toMultipartData()).block();
return null;
})).configureClient().baseUrl("http://localhost").build().post()
.uri("/foo").syncBody(multipartData).exchange().expectBody()
.returnResult();
OperationRequest request = this.converter.convert(result);
assertThat(request.getUri(), is(URI.create("http://localhost/foo")));
assertThat(request.getMethod(), is(HttpMethod.POST));
assertThat(request.getParts().size(), is(1));
OperationRequestPart part = request.getParts().iterator().next();
assertThat(part.getName(), is(equalTo("file")));
assertThat(part.getSubmittedFileName(), is(equalTo("image.png")));
assertThat(part.getHeaders().size(), is(3));
assertThat(part.getHeaders().getContentLength(), is(4L));
ContentDisposition contentDisposition = part.getHeaders().getContentDisposition();
assertThat(contentDisposition.getName(), is(equalTo("file")));
assertThat(contentDisposition.getFilename(), is(equalTo("image.png")));
assertThat(part.getHeaders().getContentType(), is(equalTo(MediaType.IMAGE_PNG)));
assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 })));
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.webtestclient;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.test.web.reactive.server.ExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
/**
* Tests for {@link WebTestClientResponseConverter}.
*
* @author Andy Wilkinson
*/
public class WebTestClientResponseConverterTests {
private final WebTestClientResponseConverter converter = new WebTestClientResponseConverter();
@Test
public void basicResponse() {
ExchangeResult result = WebTestClient
.bindToRouterFunction(RouterFunctions.route(GET("/foo"),
(req) -> ServerResponse.ok().syncBody("Hello, World!")))
.configureClient().baseUrl("http://localhost").build().get().uri("/foo")
.exchange().expectBody().returnResult();
OperationResponse response = this.converter.convert(result);
assertThat(response.getStatus(), is(HttpStatus.OK));
assertThat(response.getContentAsString(), is(equalTo("Hello, World!")));
assertThat(response.getHeaders().getContentType(),
is(MediaType.parseMediaType("text/plain;charset=UTF-8")));
assertThat(response.getHeaders().getContentLength(), is(13L));
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.webtestclient;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link WebTestClientRestDocumentationConfigurer}.
*
* @author Andy Wilkinson
*/
public class WebTestClientRestDocumentationConfigurerTests {
@Rule
public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
private final WebTestClientRestDocumentationConfigurer configurer = new WebTestClientRestDocumentationConfigurer(
this.restDocumentation);
@Test
public void configurationCanBeRetrievedButOnlyOnce() {
ClientRequest request = mock(ClientRequest.class);
HttpHeaders headers = new HttpHeaders();
headers.add(WebTestClient.WEBTESTCLIENT_REQUEST_ID, "1");
given(request.headers()).willReturn(headers);
this.configurer.filter(request, mock(ExchangeFunction.class));
Map<String, Object> configuration = WebTestClientRestDocumentationConfigurer
.retrieveConfiguration(headers);
assertThat(configuration, notNullValue());
assertThat(
WebTestClientRestDocumentationConfigurer.retrieveConfiguration(headers),
nullValue());
}
}

View File

@@ -0,0 +1,184 @@
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.webtestclient;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.restdocs.templates.TemplateFormats;
import org.springframework.test.web.reactive.server.EntityExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyExtractors;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.partWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
import static org.springframework.restdocs.request.RequestDocumentation.requestParts;
import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor;
import static org.springframework.restdocs.test.SnippetMatchers.snippet;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithTitleAndHeader;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;
/**
* Integration tests for using Spring REST Docs with Spring Framework's WebTestClient.
*
* @author Andy Wilkinson
*/
public class WebTestClientRestDocumentationIntegrationTests {
@Rule
public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
private WebTestClient webTestClient;
@Before
public void setUp() {
RouterFunction<ServerResponse> route = RouterFunctions
.route(RequestPredicates.GET("/"),
(request) -> ServerResponse.status(HttpStatus.OK)
.body(fromObject(new Person("Jane", "Doe"))))
.andRoute(RequestPredicates.GET("/{foo}/{bar}"),
(request) -> ServerResponse.status(HttpStatus.OK)
.body(fromObject(new Person("Jane", "Doe"))))
.andRoute(RequestPredicates.POST("/upload"), (request) -> {
return request.body(BodyExtractors.toMultipartData()).map((parts) -> {
return ServerResponse.status(HttpStatus.OK).build().block();
});
});
this.webTestClient = WebTestClient.bindToRouterFunction(route).configureClient()
.baseUrl("https://api.example.com")
.filter(documentationConfiguration(this.restDocumentation)).build();
}
@Test
public void defaultSnippetGeneration() {
File outputDir = new File("build/generated-snippets/default-snippets");
FileSystemUtils.deleteRecursively(outputDir);
this.webTestClient.get().uri("/").exchange().expectStatus().isOk().expectBody()
.consumeWith(document("default-snippets"));
assertExpectedSnippetFilesExist(outputDir, "http-request.adoc",
"http-response.adoc", "curl-request.adoc", "httpie-request.adoc",
"request-body.adoc", "response-body.adoc");
}
@Test
public void pathParametersSnippet() {
this.webTestClient.get().uri("/{foo}/{bar}", "1", "2").exchange().expectStatus()
.isOk().expectBody()
.consumeWith(document("path-parameters", pathParameters(
parameterWithName("foo").description("Foo description"),
parameterWithName("bar").description("Bar description"))));
assertThat(
new File("build/generated-snippets/path-parameters/path-parameters.adoc"),
is(snippet(asciidoctor()).withContents(
tableWithTitleAndHeader(TemplateFormats.asciidoctor(),
"/{foo}/{bar}", "Parameter", "Description")
.row("`foo`", "Foo description")
.row("`bar`", "Bar description"))));
}
@Test
public void requestParametersSnippet() {
this.webTestClient.get().uri("/?a=alpha&b=bravo").exchange().expectStatus().isOk()
.expectBody()
.consumeWith(document("request-parameters", requestParameters(
parameterWithName("a").description("Alpha description"),
parameterWithName("b").description("Bravo description"))));
assertThat(
new File(
"build/generated-snippets/request-parameters/request-parameters.adoc"),
is(snippet(asciidoctor()).withContents(
tableWithHeader(TemplateFormats.asciidoctor(), "Parameter",
"Description").row("`a`", "Alpha description").row("`b`",
"Bravo description"))));
}
@Test
public void multipart() throws Exception {
MultiValueMap<String, Object> multipartData = new LinkedMultiValueMap<>();
multipartData.add("a", "alpha");
multipartData.add("b", "bravo");
Consumer<EntityExchangeResult<byte[]>> documentation = document("multipart",
requestParts(partWithName("a").description("Part a"),
partWithName("b").description("Part b")));
this.webTestClient.post().uri("/upload").syncBody(multipartData).exchange()
.expectStatus().isOk().expectBody().consumeWith(documentation);
assertThat(new File("build/generated-snippets/multipart/request-parts.adoc"),
is(snippet(asciidoctor())
.withContents(tableWithHeader(TemplateFormats.asciidoctor(),
"Part", "Description").row("`a`", "Part a").row("`b`",
"Part b"))));
}
private void assertExpectedSnippetFilesExist(File directory, String... snippets) {
Set<File> actual = new HashSet<>(Arrays.asList(directory.listFiles()));
Set<File> expected = Stream.of(snippets)
.map((snippet) -> new File(directory, snippet))
.collect(Collectors.toSet());
assertThat(actual, equalTo(expected));
}
/**
* A person.
*/
static class Person {
private final String firstName;
private final String lastName;
Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
}
}