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"]