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 8 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 { @@ -354,12 +354,6 @@ class DefaultWebTestClient implements WebTestClient {
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
public <T> FluxExchangeResult<T> returnResult(Class<T> 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; @@ -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; @@ -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.
*
* <p>The {@code WebTestClient} has 3 setup options without a running server:
* <ul>
* <li>{@link #bindToController}
* <li>{@link #bindToApplicationContext}
* <li>{@link #bindToRouterFunction}
* </ul>
* <p>and 1 option for actual requests on a socket:
* <ul>
* <li>{@link #bindToServer()}
* </ul>
* <p>{@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.
*
* <p>See the static {@code bindToXxx} entry points for creating an instance.
*
* @author Rossen Stoyanchev
* @since 5.0
@ -156,50 +150,69 @@ public interface WebTestClient { @@ -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.
* <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) {
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.
* <p><pre class="code">
* WebTestClient client = WebTestClient.bindToServer()
* .baseUrl("http://localhost:8080")
* .build();
* </pre>
* @return chained API to customize client config
*/
static Builder bindToServer() {
return new DefaultWebTestClientBuilder();
@ -609,70 +622,73 @@ public interface WebTestClient { @@ -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 <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
*/
<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);
/**
* 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
*/
<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);
/**
* 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<T>}.
* 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<T>} stream
* of decoded objects.
*
* @see <a href="https://github.com/reactor/reactor-addons">
* https://github.com/reactor/reactor-addons</a>
* <p><strong>Note:</strong> 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.
*/
<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);
/**
* 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 { @@ -691,7 +707,8 @@ public interface WebTestClient {
<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();
@ -763,7 +780,8 @@ public interface WebTestClient { @@ -763,7 +780,8 @@ public interface WebTestClient {
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();

4
spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java

@ -39,7 +39,7 @@ public class ErrorTests { @@ -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 { @@ -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);
}

46
src/docs/asciidoc/testing-webtestclient.adoc

@ -50,7 +50,7 @@ Use this option to set up a server from a @@ -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: @@ -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")
@ -190,6 +190,35 @@ instead of `Class<T>`. @@ -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]]
=== JSON content
@ -225,22 +254,23 @@ You can also use https://github.com/jayway/JsonPath[JSONPath] expressions: @@ -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<Event> result = client.get().uri("/events")
FluxExchangeResult<MyEvent> 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<T>`, 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"]

Loading…
Cancel
Save