Browse Source

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.
pull/1549/head
Rossen Stoyanchev 9 years ago
parent
commit
d04d4bfb4d
  1. 6
      spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
  2. 140
      spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java
  3. 4
      spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java
  4. 46
      src/docs/asciidoc/testing-webtestclient.adoc

6
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); return new DefaultBodyContentSpec(entityResult);
} }
@Override
public FluxExchangeResult<DataBuffer> returnResult() {
Flux<DataBuffer> body = this.response.bodyToFlux(DataBuffer.class);
return new FluxExchangeResult<>(this.result, body, this.timeout);
}
@Override @Override
public <T> FluxExchangeResult<T> returnResult(Class<T> elementType) { public <T> FluxExchangeResult<T> returnResult(Class<T> elementType) {
Flux<T> body = this.response.bodyToFlux(elementType); Flux<T> body = this.response.bodyToFlux(elementType);

140
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.context.ApplicationContext;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.format.FormatterRegistry; import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@ -57,20 +56,15 @@ import org.springframework.web.util.UriBuilder;
import org.springframework.web.util.UriBuilderFactory; import org.springframework.web.util.UriBuilderFactory;
/** /**
* Main entry point for testing WebFlux server endpoints with an API similar to * Non-blocking, reactive client for testing web servers. It uses the reactive
* that of {@link WebClient}, and actually delegating to a {@code WebClient} * {@link WebClient} internally to perform requests and provides a fluent API
* instance, but with a focus on testing. * to verify responses.
* *
* <p>The {@code WebTestClient} has 3 setup options without a running server: * <p>{@code WebTestClient} can connect to any server over an HTTP connection.
* <ul> * It can also bind directly to WebFlux applications using mock request and
* <li>{@link #bindToController} * response objects, without the need for an HTTP server.
* <li>{@link #bindToApplicationContext} *
* <li>{@link #bindToRouterFunction} * <p>See the static {@code bindToXxx} entry points for creating an instance.
* </ul>
* <p>and 1 option for actual requests on a socket:
* <ul>
* <li>{@link #bindToServer()}
* </ul>
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 5.0 * @since 5.0
@ -156,50 +150,69 @@ public interface WebTestClient {
// Static, factory methods // Static, factory methods
/** /**
* Integration testing with a "mock" server targeting specific annotated, * Use this server setup to test one `@Controller` at a time.
* WebFlux controllers. The default configuration is the same as for * This option loads the default configuration of
* {@link org.springframework.web.reactive.config.EnableWebFlux @EnableWebFlux} * {@link org.springframework.web.reactive.config.EnableWebFlux @EnableWebFlux}.
* but can also be further customized through the returned spec. * There are builder methods to customize the Java config. The resulting
* @param controllers the controllers to test * WebFlux application will be tested without an HTTP server using a mock
* @return spec for setting up controller configuration * 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) { static ControllerSpec bindToController(Object... controllers) {
return new DefaultControllerSpec(controllers); return new DefaultControllerSpec(controllers);
} }
/** /**
* Integration testing with a "mock" server with WebFlux infrastructure * Use this option to set up a server from a {@link RouterFunction}.
* detected from an {@link ApplicationContext} such as * Internally the provided configuration is passed to
* {@code @EnableWebFlux} Java config and annotated controller Spring beans. * {@code RouterFunctions#toWebHandler}. The resulting WebFlux application
* @param applicationContext the context * will be tested without an HTTP server using a mock request and response.
* @return the {@code WebTestClient} builder * @param routerFunction the RouterFunction to test
* @see org.springframework.web.reactive.config.EnableWebFlux * @return chained API to customize server and client config; use
* {@link MockServerSpec#configureClient()} to transition to client config
*/ */
static MockServerSpec<?> bindToApplicationContext(ApplicationContext applicationContext) { static RouterFunctionSpec bindToRouterFunction(RouterFunction<?> routerFunction) {
return new ApplicationContextSpec(applicationContext); return new DefaultRouterFunctionSpec(routerFunction);
} }
/** /**
* Integration testing without a server targeting WebFlux functional endpoints. * Use this option to setup a server from the Spring configuration of your
* @param routerFunction the RouterFunction to test * application, or some subset of it. Internally the provided configuration
* @return the {@code WebTestClient} builder * 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.
* <p>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) { static MockServerSpec<?> bindToApplicationContext(ApplicationContext applicationContext) {
return new DefaultRouterFunctionSpec(routerFunction); return new ApplicationContextSpec(applicationContext);
} }
/** /**
* Integration testing with a "mock" server targeting the given WebHandler. * Integration testing with a "mock" server targeting the given WebHandler.
* @param webHandler the handler to test * @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) { static MockServerSpec<?> bindToWebHandler(WebHandler webHandler) {
return new DefaultMockServerSpec(webHandler); return new DefaultMockServerSpec(webHandler);
} }
/** /**
* Complete end-to-end integration tests with actual requests to a running server. * This server setup option allows you to connect to a running server.
* @return the {@code WebTestClient} builder * <p><pre class="code">
* WebTestClient client = WebTestClient.bindToServer()
* .baseUrl("http://localhost:8080")
* .build();
* </pre>
* @return chained API to customize client config
*/ */
static Builder bindToServer() { static Builder bindToServer() {
return new DefaultWebTestClientBuilder(); 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 { interface ResponseSpec {
/** /**
* Declare expectations on the response status. * Assertions on the response status.
*/ */
StatusAssertions expectStatus(); StatusAssertions expectStatus();
/** /**
* Declared expectations on the headers of the response. * Assertions on the headers of the response.
*/ */
HeaderAssertions expectHeader(); HeaderAssertions expectHeader();
/** /**
* Declare expectations on the response body decoded to {@code <B>}. * Consume and decode the response body to a single object of type
* {@code <B>} and then apply assertions.
* @param bodyType the expected body type * @param bodyType the expected body type
*/ */
<B> BodySpec<B, ?> expectBody(Class<B> bodyType); <B> BodySpec<B, ?> expectBody(Class<B> 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.
*/ */
<B> BodySpec<B, ?> expectBody(ParameterizedTypeReference<B> bodyType); <B> BodySpec<B, ?> expectBody(ParameterizedTypeReference<B> bodyType);
/** /**
* Declare expectations on the response body decoded to {@code List<E>}. * Consume and decode the response body to {@code List<E>} and then apply
* List-specific assertions.
* @param elementType the expected List element type * @param elementType the expected List element type
*/ */
<E> ListBodySpec<E> expectBodyList(Class<E> elementType); <E> ListBodySpec<E> expectBodyList(Class<E> 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.
*/ */
<E> ListBodySpec<E> expectBodyList(ParameterizedTypeReference<E> elementType); <E> ListBodySpec<E> expectBodyList(ParameterizedTypeReference<E> 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(); BodyContentSpec expectBody();
/** /**
* Return the exchange result with the body decoded to {@code Flux<T>}. * Exit the chained API and consume the response body externally. This
* Use this option for infinite streams and consume the stream with * is useful for testing infinite streams (e.g. SSE) where you need to
* the {@code StepVerifier} from the Reactor Add-Ons. * 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<T>} stream
* of decoded objects.
* *
* @see <a href="https://github.com/reactor/reactor-addons"> * <p><strong>Note:</strong> Do not use this option for cases where there
* https://github.com/reactor/reactor-addons</a> * 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.
*/ */
<T> FluxExchangeResult<T> returnResult(Class<T> elementType); <T> FluxExchangeResult<T> returnResult(Class<T> 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.
*/ */
<T> FluxExchangeResult<T> returnResult(ParameterizedTypeReference<T> elementType); <T> FluxExchangeResult<T> returnResult(ParameterizedTypeReference<T> elementType);
/**
* Return the exchange result with the body decoded to
* {@code Flux<DataBuffer>}. Use this option for infinite streams and
* consume the stream with the {@code StepVerifier} from the Reactor Add-Ons.
*
* @return
*/
FluxExchangeResult<DataBuffer> returnResult();
} }
/** /**
@ -691,7 +707,8 @@ public interface WebTestClient {
<T extends S> T consumeWith(Consumer<EntityExchangeResult<B>> consumer); <T extends S> T consumeWith(Consumer<EntityExchangeResult<B>> consumer);
/** /**
* Return the exchange result with the decoded body. * Exit the chained API and return an {@code ExchangeResult} with the
* decoded response content.
*/ */
EntityExchangeResult<B> returnResult(); EntityExchangeResult<B> returnResult();
@ -763,7 +780,8 @@ public interface WebTestClient {
BodyContentSpec consumeWith(Consumer<EntityExchangeResult<byte[]>> consumer); BodyContentSpec consumeWith(Consumer<EntityExchangeResult<byte[]>> 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<byte[]> returnResult(); EntityExchangeResult<byte[]> returnResult();

4
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") this.client.get().uri("/invalid")
.exchange() .exchange()
.expectStatus().isNotFound() .expectStatus().isNotFound()
.expectBody().isEmpty(); .expectBody(Void.class);
} }
@Test @Test
@ -47,7 +47,7 @@ public class ErrorTests {
this.client.get().uri("/server-error") this.client.get().uri("/server-error")
.exchange() .exchange()
.expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) .expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)
.expectBody().isEmpty(); .expectBody(Void.class);
} }

46
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`. Internally the provided configuration is passed to `RouterFunctions.toWebHandler`.
The resulting WebFlux application will be tested without an HTTP server using mock The resulting WebFlux application will be tested without an HTTP server using mock
request and response objects request and response objects.
[[webtestclient-context-config]] [[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<Person> result = client.get().uri("/persons/1") EntityExchangeResult<Person> result = client.get().uri("/persons/1")
@ -190,6 +190,35 @@ instead of `Class<T>`.
==== ====
[[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]] [[webtestclient-json]]
=== JSON content === JSON content
@ -225,22 +254,23 @@ You can also use https://github.com/jayway/JsonPath[JSONPath] expressions:
=== Streaming responses === Streaming responses
To test infinite streams (e.g. `"text/event-stream"`, `"application/stream+json"`), To test infinite streams (e.g. `"text/event-stream"`, `"application/stream+json"`),
exit the response workflow via `returnResult` immediately after response status and you'll need to exit the chained API, via `returnResult`, immediately after response status
header assertions, as shown below: and header assertions, as shown below:
[source,java,intent=0] [source,java,intent=0]
[subs="verbatim,quotes"] [subs="verbatim,quotes"]
---- ----
FluxExchangeResult<Event> result = client.get().uri("/events") FluxExchangeResult<MyEvent> result = client.get().uri("/events")
.accept(TEXT_EVENT_STREAM) .accept(TEXT_EVENT_STREAM)
.exchange() .exchange()
.expectStatus().isOk() .expectStatus().isOk()
.returnResult(Event.class); .returnResult(MyEvent.class);
---- ----
Now you can use the `StepVerifier`, from the `reactor-test` module, to apply Now you can consume the `Flux<T>`, assert decoded objects as they come, and then
assertions on the stream of decoded objects and cancel when test objectives are met: 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] [source,java,intent=0]
[subs="verbatim,quotes"] [subs="verbatim,quotes"]

Loading…
Cancel
Save