From d04d4bfb4d4bfabd7cef417d5b45fd992603a18f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 4 Oct 2017 14:58:11 -0400 Subject: [PATCH] Better "no content" support and polish in WebTestClient The WebTestClient now takes advantage of the support for decoding response to Void.class in WebClient so that applications can use expectBody(Void.class) to the same effect as using response.bodyToMono(Void.class) as documneted on WebClient#exchange. The top-level, no-arg returnResult method (added very recently) has been retracted, since the use of returnResult at that level, i.e. without consuming the response content, should be used mainly for streaming. It shouldn't be used for "no content" scenarios. Documentation and Javadoc have been udpated accordingly. --- .../reactive/server/DefaultWebTestClient.java | 6 - .../web/reactive/server/WebTestClient.java | 140 ++++++++++-------- .../reactive/server/samples/ErrorTests.java | 4 +- src/docs/asciidoc/testing-webtestclient.adoc | 46 +++++- 4 files changed, 119 insertions(+), 77 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java index 6b0e23a95d0..ff47ce25e26 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java @@ -354,12 +354,6 @@ class DefaultWebTestClient implements WebTestClient { return new DefaultBodyContentSpec(entityResult); } - @Override - public FluxExchangeResult returnResult() { - Flux body = this.response.bodyToFlux(DataBuffer.class); - return new FluxExchangeResult<>(this.result, body, this.timeout); - } - @Override public FluxExchangeResult returnResult(Class elementType) { Flux body = this.response.bodyToFlux(elementType); diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index ffc5e3b3b8c..b35b347fd47 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -29,7 +29,6 @@ import org.reactivestreams.Publisher; import org.springframework.context.ApplicationContext; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.io.buffer.DataBuffer; import org.springframework.format.FormatterRegistry; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -57,20 +56,15 @@ import org.springframework.web.util.UriBuilder; import org.springframework.web.util.UriBuilderFactory; /** - * Main entry point for testing WebFlux server endpoints with an API similar to - * that of {@link WebClient}, and actually delegating to a {@code WebClient} - * instance, but with a focus on testing. + * Non-blocking, reactive client for testing web servers. It uses the reactive + * {@link WebClient} internally to perform requests and provides a fluent API + * to verify responses. * - *

The {@code WebTestClient} has 3 setup options without a running server: - *

    - *
  • {@link #bindToController} - *
  • {@link #bindToApplicationContext} - *
  • {@link #bindToRouterFunction} - *
- *

and 1 option for actual requests on a socket: - *

    - *
  • {@link #bindToServer()} - *
+ *

{@code WebTestClient} can connect to any server over an HTTP connection. + * It can also bind directly to WebFlux applications using mock request and + * response objects, without the need for an HTTP server. + * + *

See the static {@code bindToXxx} entry points for creating an instance. * * @author Rossen Stoyanchev * @since 5.0 @@ -156,50 +150,69 @@ public interface WebTestClient { // Static, factory methods /** - * Integration testing with a "mock" server targeting specific annotated, - * WebFlux controllers. The default configuration is the same as for - * {@link org.springframework.web.reactive.config.EnableWebFlux @EnableWebFlux} - * but can also be further customized through the returned spec. - * @param controllers the controllers to test - * @return spec for setting up controller configuration + * Use this server setup to test one `@Controller` at a time. + * This option loads the default configuration of + * {@link org.springframework.web.reactive.config.EnableWebFlux @EnableWebFlux}. + * There are builder methods to customize the Java config. The resulting + * WebFlux application will be tested without an HTTP server using a mock + * request and response. + * @param controllers one or more controller instances to tests + * @return chained API to customize server and client config; use + * {@link MockServerSpec#configureClient()} to transition to client config */ static ControllerSpec bindToController(Object... controllers) { return new DefaultControllerSpec(controllers); } /** - * Integration testing with a "mock" server with WebFlux infrastructure - * detected from an {@link ApplicationContext} such as - * {@code @EnableWebFlux} Java config and annotated controller Spring beans. - * @param applicationContext the context - * @return the {@code WebTestClient} builder - * @see org.springframework.web.reactive.config.EnableWebFlux + * Use this option to set up a server from a {@link RouterFunction}. + * Internally the provided configuration is passed to + * {@code RouterFunctions#toWebHandler}. The resulting WebFlux application + * will be tested without an HTTP server using a mock request and response. + * @param routerFunction the RouterFunction to test + * @return chained API to customize server and client config; use + * {@link MockServerSpec#configureClient()} to transition to client config */ - static MockServerSpec bindToApplicationContext(ApplicationContext applicationContext) { - return new ApplicationContextSpec(applicationContext); + static RouterFunctionSpec bindToRouterFunction(RouterFunction routerFunction) { + return new DefaultRouterFunctionSpec(routerFunction); } /** - * Integration testing without a server targeting WebFlux functional endpoints. - * @param routerFunction the RouterFunction to test - * @return the {@code WebTestClient} builder + * Use this option to setup a server from the Spring configuration of your + * application, or some subset of it. Internally the provided configuration + * is passed to {@code WebHttpHandlerBuilder} to set up the request + * processing chain. The resulting WebFlux application will be tested + * without an HTTP server using a mock request and response. + *

Consider using the TestContext framework and + * {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration} + * in order to efficently load and inject the Spring configuration into the + * test class. + * @param applicationContext the Spring context + * @return chained API to customize server and client config; use + * {@link MockServerSpec#configureClient()} to transition to client config */ - static RouterFunctionSpec bindToRouterFunction(RouterFunction routerFunction) { - return new DefaultRouterFunctionSpec(routerFunction); + static MockServerSpec bindToApplicationContext(ApplicationContext applicationContext) { + return new ApplicationContextSpec(applicationContext); } /** * Integration testing with a "mock" server targeting the given WebHandler. * @param webHandler the handler to test - * @return the {@code WebTestClient} builder + * @return chained API to customize server and client config; use + * {@link MockServerSpec#configureClient()} to transition to client config */ static MockServerSpec bindToWebHandler(WebHandler webHandler) { return new DefaultMockServerSpec(webHandler); } /** - * Complete end-to-end integration tests with actual requests to a running server. - * @return the {@code WebTestClient} builder + * This server setup option allows you to connect to a running server. + *

+	 * WebTestClient client = WebTestClient.bindToServer()
+	 *         .baseUrl("http://localhost:8080")
+	 *         .build();
+	 * 
+ * @return chained API to customize client config */ static Builder bindToServer() { return new DefaultWebTestClientBuilder(); @@ -609,70 +622,73 @@ public interface WebTestClient { /** - * Spec for declaring expectations on the response. + * Chained API for applying assertions to a response. */ interface ResponseSpec { /** - * Declare expectations on the response status. + * Assertions on the response status. */ StatusAssertions expectStatus(); /** - * Declared expectations on the headers of the response. + * Assertions on the headers of the response. */ HeaderAssertions expectHeader(); /** - * Declare expectations on the response body decoded to {@code }. + * Consume and decode the response body to a single object of type + * {@code } and then apply assertions. * @param bodyType the expected body type */ BodySpec expectBody(Class bodyType); /** - * Variant of {@link #expectBody(Class)} for a body type with generics. + * Alternative to {@link #expectBody(Class)} that accepts information + * about a target type with generics. */ BodySpec expectBody(ParameterizedTypeReference bodyType); /** - * Declare expectations on the response body decoded to {@code List}. + * Consume and decode the response body to {@code List} and then apply + * List-specific assertions. * @param elementType the expected List element type */ ListBodySpec expectBodyList(Class elementType); /** - * Variant of {@link #expectBodyList(Class)} for element types with generics. + * Alternative to {@link #expectBodyList(Class)} that accepts information + * about a target type with generics. */ ListBodySpec expectBodyList(ParameterizedTypeReference elementType); /** - * Declare expectations on the response body content. + * Consume and decode the response body to {@code byte[]} and then apply + * assertions on the raw content (e.g. isEmpty, JSONPath, etc.) */ BodyContentSpec expectBody(); /** - * Return the exchange result with the body decoded to {@code Flux}. - * Use this option for infinite streams and consume the stream with - * the {@code StepVerifier} from the Reactor Add-Ons. + * Exit the chained API and consume the response body externally. This + * is useful for testing infinite streams (e.g. SSE) where you need to + * to assert decoded objects as they come and then cancel at some point + * when test objectives are met. Consider using {@code StepVerifier} + * from {@literal "reactor-test"} to assert the {@code Flux} stream + * of decoded objects. * - * @see - * https://github.com/reactor/reactor-addons + *

Note: Do not use this option for cases where there + * is no content (e.g. 204, 4xx) or you're not interested in the content. + * For such cases you can use {@code expectBody().isEmpty()} or + * {@code expectBody(Void.class)} which ensures that resources are + * released regardless of whether the response has content or not. */ FluxExchangeResult returnResult(Class elementType); /** - * Variant of {@link #returnResult(Class)} for element types with generics. + * Alternative to {@link #returnResult(Class)} that accepts information + * about a target type with generics. */ FluxExchangeResult returnResult(ParameterizedTypeReference elementType); - - /** - * Return the exchange result with the body decoded to - * {@code Flux}. Use this option for infinite streams and - * consume the stream with the {@code StepVerifier} from the Reactor Add-Ons. - * - * @return - */ - FluxExchangeResult returnResult(); } /** @@ -691,7 +707,8 @@ public interface WebTestClient { T consumeWith(Consumer> consumer); /** - * Return the exchange result with the decoded body. + * Exit the chained API and return an {@code ExchangeResult} with the + * decoded response content. */ EntityExchangeResult returnResult(); @@ -763,7 +780,8 @@ public interface WebTestClient { BodyContentSpec consumeWith(Consumer> consumer); /** - * Return the exchange result with body content as {@code byte[]}. + * Exit the chained API and return an {@code ExchangeResult} with the + * raw response content. */ EntityExchangeResult returnResult(); diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java index 823b7d2057c..b734a58e3a9 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java @@ -39,7 +39,7 @@ public class ErrorTests { this.client.get().uri("/invalid") .exchange() .expectStatus().isNotFound() - .expectBody().isEmpty(); + .expectBody(Void.class); } @Test @@ -47,7 +47,7 @@ public class ErrorTests { this.client.get().uri("/server-error") .exchange() .expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) - .expectBody().isEmpty(); + .expectBody(Void.class); } diff --git a/src/docs/asciidoc/testing-webtestclient.adoc b/src/docs/asciidoc/testing-webtestclient.adoc index e74edc6174e..dd332d668f6 100644 --- a/src/docs/asciidoc/testing-webtestclient.adoc +++ b/src/docs/asciidoc/testing-webtestclient.adoc @@ -50,7 +50,7 @@ Use this option to set up a server from a 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 +request and response objects. [[webtestclient-context-config]] @@ -171,7 +171,7 @@ You can go beyond the built-in assertions and create your own: }); ---- -You can also exit the workflow and get an `ExchangeResult` with the response data: +You can also exit the workflow and get a result: ---- EntityExchangeResult result = client.get().uri("/persons/1") @@ -190,6 +190,35 @@ instead of `Class`. ==== +[[webtestclient-no-content]] +=== No content + +If the response has no content, or you don't care if it does, use `Void.class` which ensures +that resources are released: + +[source,java,intent=0] +[subs="verbatim,quotes"] +---- + client.get().uri("/persons/123") + .exchange() + .expectStatus().isNotFound() + .expectBody(Void.class); +---- + +Or if you want to assert there is no response content, use this: + +[source,java,intent=0] +[subs="verbatim,quotes"] +---- + client.post().uri("/persons") + .body(personMono, Person.class) + .exchange() + .expectStatus().isCreated() + .expectBody().isEmpty; +---- + + + [[webtestclient-json]] === JSON content @@ -225,22 +254,23 @@ You can also use https://github.com/jayway/JsonPath[JSONPath] expressions: === 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: +you'll need to exit the chained API, 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") + FluxExchangeResult result = client.get().uri("/events") .accept(TEXT_EVENT_STREAM) .exchange() .expectStatus().isOk() - .returnResult(Event.class); + .returnResult(MyEvent.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: +Now you can consume the `Flux`, assert decoded objects as they come, and then +cancel at some point when test objects are met. We recommend using the `StepVerifier` +from the `reactor-test` module to do that, for example: [source,java,intent=0] [subs="verbatim,quotes"]