diff --git a/src/docs/asciidoc/testing-webtestclient.adoc b/src/docs/asciidoc/testing-webtestclient.adoc new file mode 100644 index 0000000000..e74edc6174 --- /dev/null +++ b/src/docs/asciidoc/testing-webtestclient.adoc @@ -0,0 +1,259 @@ +[[webtestclient]] += WebTestClient + +`WebTestClient` is a non-blocking, reactive client for testing web servers. It uses +the reactive <> internally to perform +requests and provides a fluent API to verify responses. The `WebTestClient` can connect +to any server over an HTTP connection. It can also bind directly to WebFlux applications +with <> objects, +without the need for an HTTP server. + + + +[[webtestclient-setup]] +== Setup + +To create a `WebTestClient` you must choose one of several server setup options. +Effectively you either configure a WebFlux application to bind to, or use absolute URLs +to connect to a running server. + + +[[webtestclient-controller-config]] +=== Bind to controller + +Use this server setup to test one `@Controller` at a time: + +[source,java,intent=0] +[subs="verbatim,quotes"] +---- + client = WebTestClient.bindToController(new TestController()).build(); +---- + +The above loads the <> and +registers the given controller. The resulting WebFlux application will be tested +without an HTTP server using mock request and response objects. There are more methods +on the builder to customize the default WebFlux Java config. + + +[[webtestclient-fn-config]] +=== Bind to RouterFunction + +Use this option to set up a server from a +<>: + +[source,java,intent=0] +[subs="verbatim,quotes"] +---- + RouterFunction route = ... + client = WebTestClient.bindToRouterFunction(route).build(); +---- + +Internally the provided configuration is passed to `RouterFunctions.toWebHandler`. +The resulting WebFlux application will be tested without an HTTP server using mock +request and response objects + + +[[webtestclient-context-config]] +=== Bind to ApplicationContext + +Use this option to setup a server from the Spring configuration of your application, or +some subset of it: + +[source,java,intent=0] +[subs="verbatim,quotes"] +---- + @RunWith(SpringRunner.class) + @ContextConfiguration(classes = WebConfig.class) // <1> + public class MyTests { + + @Autowired + private ApplicationContext context; // <2> + + private WebTestClient client; + + @Before + public void setUp() { + client = WebTestClient.bindToApplicationContext(context).build(); // <3> + } + + } +---- + +<1> Specify the configuration to load +<2> Inject the configuration +<3> Create the `WebTestClient` + +Internally the provided configuration is passed to `WebHttpHandlerBuilder` to set up +the request processing chain, see +<> for more details. The +resulting WebFlux application will be tested without an HTTP server using mock request +and response objects. + + +[[webtestclient-server-config]] +=== Bind to server + +This server setup option allows you to connect to a running server: + +[source,java,intent=0] +[subs="verbatim,quotes"] +---- + client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build(); +---- + + +[[webtestclient-client-config]] +=== Client builder + +In addition to the server setup options above, you can also configure client +options including base URL, default headers, client filters, and others. These options +are readily available following `bindToServer`. For all others, you need to use +`configureClient()` to transition from server to client configuration as shown below: + +[source,java,intent=0] +[subs="verbatim,quotes"] +---- + client = WebTestClient.bindToController(new TestController()) + .configureClient() + .baseUrl("/test") + .build(); +---- + + + + +[[webtestclient-tests]] +== Writing tests + +`WebTestClient` is a thin shell around <>. +It provides an identical API up to the point of performing a request via `exchange()`. +What follows after `exchange()` is a chained API workflow to verify responses. + +Typically you start by asserting the response status and headers: + +[source,java,intent=0] +[subs="verbatim,quotes"] +---- + client.get().uri("/persons/1") + .accept(MediaType.APPLICATION_JSON_UTF8) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8) + // ... +---- + +Then you specify how to decode and consume the response body: + +* `expectBody(Class)` -- decode to single object. +* `expectBodyList(Class)` -- decode and collect objects to `List`. +* `expectBody()` -- decode to `byte[]` for <> or empty body. + +Then you can use built-in assertions for the body. Here is one example: + +[source,java,intent=0] +[subs="verbatim,quotes"] +---- + client.get().uri("/persons") + .exchange() + .expectStatus().isOk() + .expectBodyList(Person.class).hasSize(3).contains(person); +---- + +You can go beyond the built-in assertions and create your own: + +---- + client.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectBody(Person.class) + .consumeWith(result -> { + // custom assertions (e.g. AssertJ)... + }); +---- + +You can also exit the workflow and get an `ExchangeResult` with the response data: + +---- + EntityExchangeResult result = client.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectBody(Person.class) + .returnResult(); +---- + +[TIP] +==== +When you need to decode to a target type with generics, look for the overloaded methods +that accept +{api-spring-framework}/core/ParameterizedTypeReference.html[ParameterizedTypeReference] +instead of `Class`. +==== + + +[[webtestclient-json]] +=== JSON content + +When you use `expectBody()` the response is consumed as a `byte[]`. This is useful for +raw content assertions. For example you can use +http://jsonassert.skyscreamer.org[JSONAssert] to verify JSON content: + +[source,java,intent=0] +[subs="verbatim,quotes"] +---- + client.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectBody() + .json("{\"name\":\"Jane\"}") +---- + +You can also use https://github.com/jayway/JsonPath[JSONPath] expressions: + +[source,java,intent=0] +[subs="verbatim,quotes"] +---- + client.get().uri("/persons") + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$[0].name").isEqualTo("Jane") + .jsonPath("$[1].name").isEqualTo("Jason"); +---- + + +[[webtestclient-stream]] +=== Streaming responses + +To test infinite streams (e.g. `"text/event-stream"`, `"application/stream+json"`), +exit the response workflow via `returnResult` immediately after response status and +header assertions, as shown below: + +[source,java,intent=0] +[subs="verbatim,quotes"] +---- + FluxExchangeResult result = client.get().uri("/events") + .accept(TEXT_EVENT_STREAM) + .exchange() + .expectStatus().isOk() + .returnResult(Event.class); + +---- + +Now you can use the `StepVerifier`, from the `reactor-test` module, to apply +assertions on the stream of decoded objects and cancel when test objectives are met: + +[source,java,intent=0] +[subs="verbatim,quotes"] +---- + Flux eventFux = result.getResponseBody(); + + StepVerifier.create(eventFlux) + .expectNext(person) + .expectNextCount(4) + .consumeNextWith(p -> ...) + .thenCancel() + .verify(); +---- + + + diff --git a/src/docs/asciidoc/testing.adoc b/src/docs/asciidoc/testing.adoc index 0b775b81fc..0412563133 100644 --- a/src/docs/asciidoc/testing.adoc +++ b/src/docs/asciidoc/testing.adoc @@ -92,13 +92,26 @@ integration testing framework for Spring MVC. See [[mock-objects-web-reactive]] ==== Spring Web Reactive -The `org.springframework.mock.http.server.reactive` package contains mock request and -response objects for testing _Spring WebFlux_ applications. There is also a -`MockWebServerExchange` in the `org.springframework.mock.web.server` package that uses -those mock request and response objects. +The package `org.springframework.mock.http.server.reactive` contains mock +implementations of `ServerHttpRequest` and `ServerHttpResponse` for use in WebFlux +applications. The package `org.springframework.mock.web.server` +contains a mock `ServerWebExchange` that depends on those mock request and response +objects. -`WebTestClient` builds on these mock objects to provide integration testing support for -WebFlux server endpoints without running an HTTP server. +Both `MockServerHttpRequest` and `MockServerHttpResponse` extend from the same +abstract base classes as server-specific implementations do and share behavior with them. +For example a mock request is immutable once created but you can use the `mutate()` method +from `ServerHttpRequest` to create a modified instance. + +In order for the mock response to properly implement the write contract and return a +write completion handle (i.e. `Mono`), by default it uses a `Flux` with +`cache().then()`, which buffers the data and makes it available for assertions in +tests. Applications can set a custom write function for example to test an infinite +stream. + +The <> builds on the mock request and response to provide support for +testing WebFlux applications without an HTTP server. The client can also be used for +end-to-end tests with a running server. [[unit-testing-support-classes]] @@ -5553,6 +5566,8 @@ https://github.com/spring-projects/spring-framework/tree/master/spring-test/src/ tests] of client-side REST tests. +include::testing-webtestclient.adoc[leveloffset=+2] + [[testing-examples-petclinic]] === PetClinic Example diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index 34658fde23..7fd0fdc0fb 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -16,9 +16,10 @@ and co-exist side by side in the Spring Framework. Each module is optional. Applications may use one or the other module, or in some cases both -- e.g. Spring MVC controllers with the reactive `WebClient`. -In addition to the web framework, Spring WebFlux also provides a <> for -performing HTTP requests, a `WebTestClient` for testing web endpoints, and -also client and server reactive, WebSocket support. +The `spring-webflux` module also provides a reactive <> for +performing HTTP requests, along with client and server, reactive WebSocket support. +The `spring-test` module provides test support for WebFlux applications, see +<> for more details. [[webflux-new-framework]] @@ -1389,6 +1390,19 @@ the classpath. include::webflux-webclient.adoc[leveloffset=+1] +[[webflux-test]] +== Testing + +The `spring-test` module provides mock implementations of `ServerHttpRequest`, +`ServerHttpResponse`, and `ServerWebExchange`. +See <> mock objects. + +The <> builds on these mock request and +response objects to provide support for testing WebFlux applications without and HTTP +server. The `WebTestClient` can be used for end-to-end integration tests too. + + + [[webflux-reactive-libraries]] == Reactive Libraries